mirror of
https://git.joinsharkey.org/Sharkey/Sharkey.git
synced 2024-12-23 14:03:10 +02:00
Instance Ticker
This commit is contained in:
parent
25bd82ecaa
commit
615fedd64d
14 changed files with 280 additions and 25 deletions
|
@ -597,6 +597,12 @@ openInNewTab: "新しいタブで開く"
|
|||
openInSideView: "サイドビューで開く"
|
||||
defaultNavigationBehaviour: "デフォルトのナビゲーション"
|
||||
editTheseSettingsMayBreakAccount: "これらの設定を編集するとアカウントが破損する可能性があります。"
|
||||
instanceTicker: "ノートのインスタンス情報"
|
||||
|
||||
_instanceTicker:
|
||||
none: "表示しない"
|
||||
remote: "リモートユーザーに表示"
|
||||
always: "常に表示"
|
||||
|
||||
_serverDisconnectedBehavior:
|
||||
reload: "自動でリロード"
|
||||
|
|
14
migration/1603776877564-instance-theme-color.ts
Normal file
14
migration/1603776877564-instance-theme-color.ts
Normal 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"`);
|
||||
}
|
||||
|
||||
}
|
14
migration/1603781553011-instance-favicon.ts
Normal file
14
migration/1603781553011-instance-favicon.ts
Normal 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"`);
|
||||
}
|
||||
|
||||
}
|
61
src/client/components/instance-ticker.vue
Normal file
61
src/client/components/instance-ticker.vue
Normal 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>
|
|
@ -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;
|
||||
}
|
||||
},
|
||||
|
||||
|
|
|
@ -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,
|
||||
}, {
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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 }); }
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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 }">
|
||||
|
|
|
@ -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,
|
||||
})
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue