diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index f4c22d1ff..58e9132cf 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 000000000..80c9d516f --- /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 000000000..d748c43f5 --- /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 000000000..9447e6d4c --- /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 8ddb01f73..4e31aec12 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 3ceb1f9b8..bd19cf1a7 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 ac8d7d952..e0d2fd1de 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 d61d8620e..0db571ff1 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 5dc35bb42..5c6c71d4f 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 8b7dfd791..8a3c19b63 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 5fedfc095..7c8719e06 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 7ea4d42bc..4ac7c6d85 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 d3f0106ac..9652d29db 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 41fef859c..487421816 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; +}