From 615fedd64d269c896410713409847ecc4b1c9671 Mon Sep 17 00:00:00 2001 From: syuilo Date: Tue, 27 Oct 2020 16:16:59 +0900 Subject: [PATCH] Instance Ticker --- locales/ja-JP.yml | 6 + .../1603776877564-instance-theme-color.ts | 14 ++ migration/1603781553011-instance-favicon.ts | 14 ++ src/client/components/instance-ticker.vue | 61 +++++++ src/client/components/note.vue | 8 + src/client/components/sidebar.vue | 2 +- src/client/config.ts | 2 +- src/client/pages/settings/general.vue | 11 ++ src/client/store.ts | 1 + src/client/ui/visitor.vue | 4 +- src/models/entities/instance.ts | 10 ++ src/models/repositories/user.ts | 10 +- src/server/web/views/base.pug | 1 + src/services/fetch-instance-metadata.ts | 161 +++++++++++++++--- 14 files changed, 280 insertions(+), 25 deletions(-) create mode 100644 migration/1603776877564-instance-theme-color.ts create mode 100644 migration/1603781553011-instance-favicon.ts create mode 100644 src/client/components/instance-ticker.vue diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index f4c22d1fff..58e9132cf3 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -597,6 +597,12 @@ openInNewTab: "新しいタブで開く" openInSideView: "サイドビューで開く" defaultNavigationBehaviour: "デフォルトのナビゲーション" editTheseSettingsMayBreakAccount: "これらの設定を編集するとアカウントが破損する可能性があります。" +instanceTicker: "ノートのインスタンス情報" + +_instanceTicker: + none: "表示しない" + remote: "リモートユーザーに表示" + always: "常に表示" _serverDisconnectedBehavior: reload: "自動でリロード" diff --git a/migration/1603776877564-instance-theme-color.ts b/migration/1603776877564-instance-theme-color.ts new file mode 100644 index 0000000000..80c9d516fc --- /dev/null +++ b/migration/1603776877564-instance-theme-color.ts @@ -0,0 +1,14 @@ +import {MigrationInterface, QueryRunner} from "typeorm"; + +export class instanceThemeColor1603776877564 implements MigrationInterface { + name = 'instanceThemeColor1603776877564' + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "instance" ADD "themeColor" character varying(64) DEFAULT null`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "instance" DROP COLUMN "themeColor"`); + } + +} diff --git a/migration/1603781553011-instance-favicon.ts b/migration/1603781553011-instance-favicon.ts new file mode 100644 index 0000000000..d748c43f5e --- /dev/null +++ b/migration/1603781553011-instance-favicon.ts @@ -0,0 +1,14 @@ +import {MigrationInterface, QueryRunner} from "typeorm"; + +export class instanceFavicon1603781553011 implements MigrationInterface { + name = 'instanceFavicon1603781553011' + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "instance" ADD "faviconUrl" character varying(256) DEFAULT null`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "instance" DROP COLUMN "faviconUrl"`); + } + +} diff --git a/src/client/components/instance-ticker.vue b/src/client/components/instance-ticker.vue new file mode 100644 index 0000000000..9447e6d4c3 --- /dev/null +++ b/src/client/components/instance-ticker.vue @@ -0,0 +1,61 @@ + + + + + diff --git a/src/client/components/note.vue b/src/client/components/note.vue index 8ddb01f733..4e31aec12e 100644 --- a/src/client/components/note.vue +++ b/src/client/components/note.vue @@ -40,6 +40,7 @@
+

@@ -139,6 +140,7 @@ export default defineComponent({ XCwButton, XPoll, MkUrlPreview: defineAsyncComponent(() => import('@/components/url-preview.vue')), + MkInstanceTicker: defineAsyncComponent(() => import('@/components/instance-ticker.vue')), }, inject: { @@ -258,6 +260,12 @@ export default defineComponent({ } else { return null; } + }, + + showTicker() { + if (this.$store.state.device.instanceTicker === 'always') return true; + if (this.$store.state.device.instanceTicker === 'remote' && this.appearNote.user.instance) return true; + return false; } }, diff --git a/src/client/components/sidebar.vue b/src/client/components/sidebar.vue index 3ceb1f9b8d..bd19cf1a74 100644 --- a/src/client/components/sidebar.vue +++ b/src/client/components/sidebar.vue @@ -246,7 +246,7 @@ export default defineComponent({ icon: faQuestionCircle, }, { type: 'link', - text: this.$t('aboutX', { x: instanceName || host }), + text: this.$t('aboutX', { x: instanceName }), to: '/about', icon: faInfoCircle, }, { diff --git a/src/client/config.ts b/src/client/config.ts index ac8d7d9528..e0d2fd1deb 100644 --- a/src/client/config.ts +++ b/src/client/config.ts @@ -12,5 +12,5 @@ export const lang = localStorage.getItem('lang'); export const langs = _LANGS_; export const getLocale = async () => Object.fromEntries((await entries(clientDb.i18n)) as [string, string][]); export const version = _VERSION_; -export const instanceName = siteName === 'Misskey' ? null : siteName; +export const instanceName = siteName === 'Misskey' ? host : siteName; export const deckmode = localStorage.getItem('deckmode') === 'true'; diff --git a/src/client/pages/settings/general.vue b/src/client/pages/settings/general.vue index d61d8620e7..0db571ff14 100644 --- a/src/client/pages/settings/general.vue +++ b/src/client/pages/settings/general.vue @@ -51,6 +51,12 @@ Aa Aa

+
+
{{ $t('instanceTicker') }}
+ {{ $t('_instanceTicker.none') }} + {{ $t('_instanceTicker.remote') }} + {{ $t('_instanceTicker.always') }} +
@@ -169,6 +175,11 @@ export default defineComponent({ set(value) { this.$store.commit('device/set', { key: 'chatOpenBehavior', value }); } }, + instanceTicker: { + get() { return this.$store.state.device.instanceTicker; }, + set(value) { this.$store.commit('device/set', { key: 'instanceTicker', value }); } + }, + enableInfiniteScroll: { get() { return this.$store.state.device.enableInfiniteScroll; }, set(value) { this.$store.commit('device/set', { key: 'enableInfiniteScroll', value }); } diff --git a/src/client/store.ts b/src/client/store.ts index 5dc35bb42e..5c6c71d4f2 100644 --- a/src/client/store.ts +++ b/src/client/store.ts @@ -77,6 +77,7 @@ export const defaultDeviceSettings = { enableInfiniteScroll: true, useBlurEffectForModal: true, sidebarDisplay: 'full', // full, icon, hide + instanceTicker: 'remote', // none, remote, always roomGraphicsQuality: 'medium', roomUseOrthographicCamera: true, deckColumnAlign: 'left', diff --git a/src/client/ui/visitor.vue b/src/client/ui/visitor.vue index 8b7dfd7911..8a3c19b631 100644 --- a/src/client/ui/visitor.vue +++ b/src/client/ui/visitor.vue @@ -4,11 +4,11 @@ {{ $t('home') }} {{ $t('announcements') }} {{ $t('channel') }} - {{ $t('aboutX', { x: instanceName || host }) }} + {{ $t('aboutX', { x: instanceName }) }}
diff --git a/src/models/entities/instance.ts b/src/models/entities/instance.ts index 5fedfc0956..7c8719e06a 100644 --- a/src/models/entities/instance.ts +++ b/src/models/entities/instance.ts @@ -163,6 +163,16 @@ export class Instance { }) public iconUrl: string | null; + @Column('varchar', { + length: 256, nullable: true, default: null, + }) + public faviconUrl: string | null; + + @Column('varchar', { + length: 64, nullable: true, default: null, + }) + public themeColor: string | null; + @Column('timestamp with time zone', { nullable: true, }) diff --git a/src/models/repositories/user.ts b/src/models/repositories/user.ts index 7ea4d42bcb..4ac7c6d85d 100644 --- a/src/models/repositories/user.ts +++ b/src/models/repositories/user.ts @@ -1,7 +1,7 @@ import $ from 'cafy'; import { EntityRepository, Repository, In, Not } from 'typeorm'; import { User, ILocalUser, IRemoteUser } from '../entities/user'; -import { Emojis, Notes, NoteUnreads, FollowRequests, Notifications, MessagingMessages, UserNotePinings, Followings, Blockings, Mutings, UserProfiles, UserSecurityKeys, UserGroupJoinings, Pages, Announcements, AnnouncementReads, Antennas, AntennaNotes, ChannelFollowings } from '..'; +import { Emojis, Notes, NoteUnreads, FollowRequests, Notifications, MessagingMessages, UserNotePinings, Followings, Blockings, Mutings, UserProfiles, UserSecurityKeys, UserGroupJoinings, Pages, Announcements, AnnouncementReads, Antennas, AntennaNotes, ChannelFollowings, Instances } from '..'; import { ensure } from '../../prelude/ensure'; import config from '../../config'; import { SchemaType } from '../../misc/schema'; @@ -181,6 +181,14 @@ export class UserRepository extends Repository { isModerator: user.isModerator || falsy, isBot: user.isBot || falsy, isCat: user.isCat || falsy, + instance: user.host ? Instances.findOne({ host: user.host }).then(instance => instance ? { + name: instance.name, + softwareName: instance.softwareName, + softwareVersion: instance.softwareVersion, + iconUrl: instance.iconUrl, + faviconUrl: instance.faviconUrl, + themeColor: instance.themeColor, + } : undefined) : undefined, // カスタム絵文字添付 emojis: user.emojis.length > 0 ? Emojis.find({ diff --git a/src/server/web/views/base.pug b/src/server/web/views/base.pug index d3f0106ac1..9652d29dbb 100644 --- a/src/server/web/views/base.pug +++ b/src/server/web/views/base.pug @@ -11,6 +11,7 @@ html meta(name='application-name' content='Misskey') meta(name='referrer' content='origin') meta(name='theme-color' content='#86b300') + meta(name='theme-color-orig' content='#86b300') meta(property='og:site_name' content= instanceName || 'Misskey') meta(name='viewport' content='width=device-width, initial-scale=1') link(rel='icon' href= icon || '/favicon.ico') diff --git a/src/services/fetch-instance-metadata.ts b/src/services/fetch-instance-metadata.ts index 41fef859c9..487421816a 100644 --- a/src/services/fetch-instance-metadata.ts +++ b/src/services/fetch-instance-metadata.ts @@ -1,4 +1,4 @@ -import { JSDOM } from 'jsdom'; +import { DOMWindow, JSDOM } from 'jsdom'; import fetch from 'node-fetch'; import { getJson, getHtml, getAgentByUrl } from '../misc/fetch'; import { Instance } from '../models/entities/instance'; @@ -22,9 +22,18 @@ export async function fetchInstanceMetadata(instance: Instance): Promise { logger.info(`Fetching metadata of ${instance.host} ...`); try { - const [info, icon] = await Promise.all([ + const [info, dom, manifest] = await Promise.all([ fetchNodeinfo(instance).catch(() => null), - fetchIconUrl(instance).catch(() => null), + fetchDom(instance).catch(() => null), + fetchManifest(instance).catch(() => null), + ]); + + const [favicon, icon, themeColor, name, description] = await Promise.all([ + fetchFaviconUrl(instance).catch(() => null), + fetchIconUrl(instance, dom, manifest).catch(() => null), + getThemeColor(dom, manifest).catch(() => null), + getSiteName(info, dom, manifest).catch(() => null), + getDescription(info, dom, manifest).catch(() => null), ]); logger.succ(`Successfuly fetched metadata of ${instance.host}`); @@ -34,18 +43,18 @@ export async function fetchInstanceMetadata(instance: Instance): Promise { } as Record; if (info) { - updates.softwareName = info.software.name.toLowerCase(); - updates.softwareVersion = info.software.version; + updates.softwareName = info.software?.name.toLowerCase(); + updates.softwareVersion = info.software?.version; updates.openRegistrations = info.openRegistrations; - updates.name = info.metadata ? (info.metadata.nodeName || info.metadata.name || null) : null; - updates.description = info.metadata ? (info.metadata.nodeDescription || info.metadata.description || null) : null; updates.maintainerName = info.metadata ? info.metadata.maintainer ? (info.metadata.maintainer.name || null) : null : null; updates.maintainerEmail = info.metadata ? info.metadata.maintainer ? (info.metadata.maintainer.email || null) : null : null; } - if (icon) { - updates.iconUrl = icon; - } + if (name) updates.name = name; + if (description) updates.description = description; + if (icon || favicon) updates.iconUrl = icon || favicon; + if (favicon) updates.faviconUrl = favicon; + if (themeColor) updates.themeColor = themeColor; await Instances.update(instance.id, updates); @@ -57,7 +66,25 @@ export async function fetchInstanceMetadata(instance: Instance): Promise { } } -async function fetchNodeinfo(instance: Instance): Promise> { +type NodeInfo = { + openRegistrations?: any; + software?: { + name?: any; + version?: any; + }; + metadata?: { + name?: any; + nodeName?: any; + nodeDescription?: any; + description?: any; + maintainer?: { + name?: any; + email?: any; + }; + }; +}; + +async function fetchNodeinfo(instance: Instance): Promise { logger.info(`Fetching nodeinfo of ${instance.host} ...`); try { @@ -100,8 +127,8 @@ async function fetchNodeinfo(instance: Instance): Promise> { } } -async function fetchIconUrl(instance: Instance): Promise { - logger.info(`Fetching icon URL of ${instance.host} ...`); +async function fetchDom(instance: Instance): Promise { + logger.info(`Fetching HTML of ${instance.host} ...`); const url = 'https://' + instance.host; @@ -110,16 +137,23 @@ async function fetchIconUrl(instance: Instance): Promise { const { window } = new JSDOM(html); const doc = window.document; - const hrefAppleTouchIconPrecomposed = doc.querySelector('link[rel="apple-touch-icon-precomposed"]')?.getAttribute('href'); - const hrefAppleTouchIcon = doc.querySelector('link[rel="apple-touch-icon"]')?.getAttribute('href'); - const hrefIcon = doc.querySelector('link[rel="icon"]')?.getAttribute('href'); + return doc; +} - const href = hrefAppleTouchIconPrecomposed || hrefAppleTouchIcon || hrefIcon; +async function fetchManifest(instance: Instance): Promise | null> { + const url = 'https://' + instance.host; - if (href) { - return (new URL(href, url)).href; - } + const manifestUrl = url + '/manifest.json'; + const manifest = await getJson(manifestUrl); + + return manifest; +} + +async function fetchFaviconUrl(instance: Instance): Promise { + logger.info(`Fetching favicon URL of ${instance.host} ...`); + + const url = 'https://' + instance.host; const faviconUrl = url + '/favicon.ico'; const favicon = await fetch(faviconUrl, { @@ -133,3 +167,90 @@ async function fetchIconUrl(instance: Instance): Promise { return null; } + +async function fetchIconUrl(instance: Instance, doc: DOMWindow['document'] | null, manifest: Record | null): Promise { + if (doc) { + const url = 'https://' + instance.host; + + const hrefAppleTouchIconPrecomposed = doc.querySelector('link[rel="apple-touch-icon-precomposed"]')?.getAttribute('href'); + const hrefAppleTouchIcon = doc.querySelector('link[rel="apple-touch-icon"]')?.getAttribute('href'); + const hrefIcon = doc.querySelector('link[rel="icon"]')?.getAttribute('href'); + + const href = hrefAppleTouchIconPrecomposed || hrefAppleTouchIcon || hrefIcon; + + if (href) { + return (new URL(href, url)).href; + } + } + + if (manifest && manifest.icons && manifest.icons.length > 0 && manifest.icons[0].src) { + const url = 'https://' + instance.host; + return (new URL(manifest.icons[0].src, url)).href; + } + + return null; +} + +async function getThemeColor(doc: DOMWindow['document'] | null, manifest: Record | null): Promise { + if (doc) { + const themeColor = doc.querySelector('meta[name="theme-color"]')?.getAttribute('content'); + + if (themeColor) { + return themeColor; + } + } + + if (manifest) { + return manifest.theme_color; + } + + return null; +} + +async function getSiteName(info: NodeInfo | null, doc: DOMWindow['document'] | null, manifest: Record | null): Promise { + if (info && info.metadata) { + if (info.metadata.nodeName || info.metadata.name) { + return info.metadata.nodeName || info.metadata.name; + } + } + + if (doc) { + const og = doc.querySelector('meta[property="og:title"]')?.getAttribute('content'); + + if (og) { + return og; + } + } + + if (manifest) { + return manifest?.name || manifest?.short_name; + } + + return null; +} + +async function getDescription(info: NodeInfo | null, doc: DOMWindow['document'] | null, manifest: Record | null): Promise { + if (info && info.metadata) { + if (info.metadata.nodeDescription || info.metadata.description) { + return info.metadata.nodeDescription || info.metadata.description; + } + } + + if (doc) { + const meta = doc.querySelector('meta[name="description"]')?.getAttribute('content'); + if (meta) { + return meta; + } + + const og = doc.querySelector('meta[property="og:description"]')?.getAttribute('content'); + if (og) { + return og; + } + } + + if (manifest) { + return manifest?.name || manifest?.short_name; + } + + return null; +}