Instance Ticker

This commit is contained in:
syuilo 2020-10-27 16:16:59 +09:00
parent 25bd82ecaa
commit 615fedd64d
14 changed files with 280 additions and 25 deletions

View file

@ -597,6 +597,12 @@ openInNewTab: "新しいタブで開く"
openInSideView: "サイドビューで開く"
defaultNavigationBehaviour: "デフォルトのナビゲーション"
editTheseSettingsMayBreakAccount: "これらの設定を編集するとアカウントが破損する可能性があります。"
instanceTicker: "ノートのインスタンス情報"
_instanceTicker:
none: "表示しない"
remote: "リモートユーザーに表示"
always: "常に表示"
_serverDisconnectedBehavior:
reload: "自動でリロード"

View file

@ -0,0 +1,14 @@
import {MigrationInterface, QueryRunner} from "typeorm";
export class instanceThemeColor1603776877564 implements MigrationInterface {
name = 'instanceThemeColor1603776877564'
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "instance" ADD "themeColor" character varying(64) DEFAULT null`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "instance" DROP COLUMN "themeColor"`);
}
}

View file

@ -0,0 +1,14 @@
import {MigrationInterface, QueryRunner} from "typeorm";
export class instanceFavicon1603781553011 implements MigrationInterface {
name = 'instanceFavicon1603781553011'
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "instance" ADD "faviconUrl" character varying(256) DEFAULT null`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "instance" DROP COLUMN "faviconUrl"`);
}
}

View file

@ -0,0 +1,61 @@
<template>
<div class="hpaizdrt" :style="bg">
<img v-if="info.faviconUrl" class="icon" :src="info.faviconUrl"/>
<span class="name">{{ info.name }}</span>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { instanceName } from '@/config';
export default defineComponent({
props: {
instance: {
type: Object,
required: false
},
},
data() {
return {
info: this.instance || {
faviconUrl: '/favicon.ico',
name: instanceName,
themeColor: (document.querySelector('meta[name="theme-color-orig"]') as HTMLMetaElement)?.content
}
}
},
computed: {
bg(): any {
return this.info.themeColor ? {
background: `linear-gradient(90deg, ${this.info.themeColor}, ${this.info.themeColor + '00'})`
} : null;
}
}
});
</script>
<style lang="scss" scoped>
.hpaizdrt {
$height: 1.1rem;
height: $height;
border-radius: 4px 0 0 4px;
overflow: hidden;
color: #fff;
> .icon {
height: 100%;
}
> .name {
margin-left: 4px;
line-height: $height;
font-size: 0.9em;
vertical-align: top;
font-weight: bold;
}
}
</style>

View file

@ -40,6 +40,7 @@
<MkAvatar class="avatar" :user="appearNote.user"/>
<div class="main">
<XNoteHeader class="header" :note="appearNote" :mini="true"/>
<MkInstanceTicker v-if="showTicker" class="ticker" :instance="appearNote.user.instance"/>
<div class="body" ref="noteBody">
<p v-if="appearNote.cw != null" class="cw">
<Mfm v-if="appearNote.cw != ''" class="text" :text="appearNote.cw" :author="appearNote.user" :i="$store.state.i" :custom-emojis="appearNote.emojis"/>
@ -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;
}
},

View file

@ -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,
}, {

View file

@ -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';

View file

@ -51,6 +51,12 @@
<MkRadio v-model="fontSize" value="large"><span style="font-size: 18px;">Aa</span></MkRadio>
<MkRadio v-model="fontSize" value="veryLarge"><span style="font-size: 20px;">Aa</span></MkRadio>
</div>
<div class="_content">
<div>{{ $t('instanceTicker') }}</div>
<MkRadio v-model="instanceTicker" value="none">{{ $t('_instanceTicker.none') }}</MkRadio>
<MkRadio v-model="instanceTicker" value="remote">{{ $t('_instanceTicker.remote') }}</MkRadio>
<MkRadio v-model="instanceTicker" value="always">{{ $t('_instanceTicker.always') }}</MkRadio>
</div>
</section>
<section class="_card _vMargin">
@ -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 }); }

View file

@ -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',

View file

@ -4,11 +4,11 @@
<MkA class="link" to="/">{{ $t('home') }}</MkA>
<MkA class="link" to="/announcements">{{ $t('announcements') }}</MkA>
<MkA class="link" to="/channels">{{ $t('channel') }}</MkA>
<MkA class="link" to="/about">{{ $t('aboutX', { x: instanceName || host }) }}</MkA>
<MkA class="link" to="/about">{{ $t('aboutX', { x: instanceName }) }}</MkA>
</header>
<div class="banner" :style="{ backgroundImage: `url(${ $store.state.instance.meta.bannerUrl })` }">
<h1>{{ instanceName || host }}</h1>
<h1>{{ instanceName }}</h1>
</div>
<div class="contents" ref="contents" :class="{ wallpaper }">

View file

@ -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,
})

View file

@ -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<User> {
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({

View file

@ -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')

View file

@ -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<void> {
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<void> {
} as Record<string, any>;
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<void> {
}
}
async function fetchNodeinfo(instance: Instance): Promise<Record<string, any>> {
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<NodeInfo> {
logger.info(`Fetching nodeinfo of ${instance.host} ...`);
try {
@ -100,8 +127,8 @@ async function fetchNodeinfo(instance: Instance): Promise<Record<string, any>> {
}
}
async function fetchIconUrl(instance: Instance): Promise<string | null> {
logger.info(`Fetching icon URL of ${instance.host} ...`);
async function fetchDom(instance: Instance): Promise<DOMWindow['document']> {
logger.info(`Fetching HTML of ${instance.host} ...`);
const url = 'https://' + instance.host;
@ -110,16 +137,23 @@ async function fetchIconUrl(instance: Instance): Promise<string | null> {
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<Record<string, any> | 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<string | null> {
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<string | null> {
return null;
}
async function fetchIconUrl(instance: Instance, doc: DOMWindow['document'] | null, manifest: Record<string, any> | null): Promise<string | null> {
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<string, any> | null): Promise<string | null> {
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<string, any> | null): Promise<string | null> {
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<string, any> | null): Promise<string | null> {
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;
}