This commit is contained in:
syuilo 2025-08-26 13:58:57 +09:00
parent d6a1046361
commit 05cc8047fa
6 changed files with 50 additions and 29 deletions

View File

@ -297,76 +297,97 @@ function prepend(note: Misskey.entities.Note & MisskeyEntity) {
} }
} }
let connection: Misskey.IChannelConnection | null = null;
let connection2: Misskey.IChannelConnection | null = null;
const stream = store.s.realtimeMode ? useStream() : null; const stream = store.s.realtimeMode ? useStream() : null;
const connections = {
antenna: null as Misskey.IChannelConnection<Misskey.Channels['antenna']> | null,
homeTimeline: null as Misskey.IChannelConnection<Misskey.Channels['homeTimeline']> | null,
localTimeline: null as Misskey.IChannelConnection<Misskey.Channels['localTimeline']> | null,
hybridTimeline: null as Misskey.IChannelConnection<Misskey.Channels['hybridTimeline']> | null,
globalTimeline: null as Misskey.IChannelConnection<Misskey.Channels['globalTimeline']> | null,
main: null as Misskey.IChannelConnection<Misskey.Channels['main']> | null,
userList: null as Misskey.IChannelConnection<Misskey.Channels['userList']> | null,
channel: null as Misskey.IChannelConnection<Misskey.Channels['channel']> | null,
roleTimeline: null as Misskey.IChannelConnection<Misskey.Channels['roleTimeline']> | null,
};
function connectChannel() { function connectChannel() {
if (stream == null) return; if (stream == null) return;
if (props.src === 'antenna') { if (props.src === 'antenna') {
if (props.antenna == null) return; if (props.antenna == null) return;
connection = stream.useChannel('antenna', { connections.antenna = stream.useChannel('antenna', {
antennaId: props.antenna, antennaId: props.antenna,
}); });
connections.antenna.on('note', prepend);
} else if (props.src === 'home') { } else if (props.src === 'home') {
connection = stream.useChannel('homeTimeline', { connections.homeTimeline = stream.useChannel('homeTimeline', {
withRenotes: props.withRenotes, withRenotes: props.withRenotes,
withFiles: props.onlyFiles ? true : undefined, withFiles: props.onlyFiles ? true : undefined,
}); });
connection2 = stream.useChannel('main'); connections.main = stream.useChannel('main');
connections.homeTimeline.on('note', prepend);
} else if (props.src === 'local') { } else if (props.src === 'local') {
connection = stream.useChannel('localTimeline', { connections.localTimeline = stream.useChannel('localTimeline', {
withRenotes: props.withRenotes, withRenotes: props.withRenotes,
withReplies: props.withReplies, withReplies: props.withReplies,
withFiles: props.onlyFiles ? true : undefined, withFiles: props.onlyFiles ? true : undefined,
}); });
connections.localTimeline.on('note', prepend);
} else if (props.src === 'social') { } else if (props.src === 'social') {
connection = stream.useChannel('hybridTimeline', { connections.hybridTimeline = stream.useChannel('hybridTimeline', {
withRenotes: props.withRenotes, withRenotes: props.withRenotes,
withReplies: props.withReplies, withReplies: props.withReplies,
withFiles: props.onlyFiles ? true : undefined, withFiles: props.onlyFiles ? true : undefined,
}); });
connections.hybridTimeline.on('note', prepend);
} else if (props.src === 'global') { } else if (props.src === 'global') {
connection = stream.useChannel('globalTimeline', { connections.globalTimeline = stream.useChannel('globalTimeline', {
withRenotes: props.withRenotes, withRenotes: props.withRenotes,
withFiles: props.onlyFiles ? true : undefined, withFiles: props.onlyFiles ? true : undefined,
}); });
connections.globalTimeline.on('note', prepend);
} else if (props.src === 'mentions') { } else if (props.src === 'mentions') {
connection = stream.useChannel('main'); connections.main = stream.useChannel('main');
connection.on('mention', prepend); connections.main.on('mention', prepend);
} else if (props.src === 'directs') { } else if (props.src === 'directs') {
const onNote = note => { const onNote = note => {
if (note.visibility === 'specified') { if (note.visibility === 'specified') {
prepend(note); prepend(note);
} }
}; };
connection = stream.useChannel('main'); connections.main = stream.useChannel('main');
connection.on('mention', onNote); connections.main.on('mention', onNote);
} else if (props.src === 'list') { } else if (props.src === 'list') {
if (props.list == null) return; if (props.list == null) return;
connection = stream.useChannel('userList', { connections.userList = stream.useChannel('userList', {
withRenotes: props.withRenotes, withRenotes: props.withRenotes,
withFiles: props.onlyFiles ? true : undefined, withFiles: props.onlyFiles ? true : undefined,
listId: props.list, listId: props.list,
}); });
connections.userList.on('note', prepend);
} else if (props.src === 'channel') { } else if (props.src === 'channel') {
if (props.channel == null) return; if (props.channel == null) return;
connection = stream.useChannel('channel', { connections.channel = stream.useChannel('channel', {
channelId: props.channel, channelId: props.channel,
}); });
connections.channel.on('note', prepend);
} else if (props.src === 'role') { } else if (props.src === 'role') {
if (props.role == null) return; if (props.role == null) return;
connection = stream.useChannel('roleTimeline', { connections.roleTimeline = stream.useChannel('roleTimeline', {
roleId: props.role, roleId: props.role,
}); });
connections.roleTimeline.on('note', prepend);
} }
if (props.src !== 'directs' && props.src !== 'mentions') connection?.on('note', prepend);
} }
function disconnectChannel() { function disconnectChannel() {
if (connection) connection.dispose(); for (const key in connections) {
if (connection2) connection2.dispose(); const conn = connections[key as keyof typeof connections];
if (conn != null) {
conn.dispose();
connections[key as keyof typeof connections] = null;
}
}
} }
if (store.s.realtimeMode) { if (store.s.realtimeMode) {

View File

@ -54,7 +54,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</MkFukidashi> </MkFukidashi>
</div> </div>
<div v-if="user.roles.length > 0" class="roles"> <div v-if="user.roles.length > 0" class="roles">
<span v-for="role in user.roles" :key="role.id" v-tooltip="role.description" class="role" :style="{ '--color': role.color }"> <span v-for="role in user.roles" :key="role.id" v-tooltip="role.description" class="role" :style="{ '--color': role.color ?? '' }">
<MkA v-adaptive-bg :to="`/roles/${role.id}`"> <MkA v-adaptive-bg :to="`/roles/${role.id}`">
<img v-if="role.iconUrl" style="height: 1.3em; vertical-align: -22%;" :src="role.iconUrl"/> <img v-if="role.iconUrl" style="height: 1.3em; vertical-align: -22%;" :src="role.iconUrl"/>
{{ role.name }} {{ role.name }}
@ -249,7 +249,7 @@ const style = computed(() => {
}); });
const age = computed(() => { const age = computed(() => {
return calcAge(props.user.birthday); return props.user.birthday ? calcAge(props.user.birthday) : NaN;
}); });
function menu(ev: MouseEvent) { function menu(ev: MouseEvent) {

View File

@ -8,7 +8,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<div :class="$style.body"> <div :class="$style.body">
<div :class="$style.top"> <div :class="$style.top">
<button v-tooltip.noDelay.right="instance.name ?? i18n.ts.instance" class="_button" :class="$style.instance" @click="openInstanceMenu"> <button v-tooltip.noDelay.right="instance.name ?? i18n.ts.instance" class="_button" :class="$style.instance" @click="openInstanceMenu">
<img :src="instance.iconUrl || instance.faviconUrl || '/favicon.ico'" alt="" :class="$style.instanceIcon" style="viewTransitionName: navbar-serverIcon;"/> <img :src="instance.iconUrl || '/favicon.ico'" alt="" :class="$style.instanceIcon" style="viewTransitionName: navbar-serverIcon;"/>
</button> </button>
<button v-if="!iconOnly" v-tooltip.noDelay.right="i18n.ts.realtimeMode" class="_button" :class="[$style.realtimeMode, store.r.realtimeMode.value ? $style.on : null]" @click="toggleRealtimeMode"> <button v-if="!iconOnly" v-tooltip.noDelay.right="i18n.ts.realtimeMode" class="_button" :class="[$style.realtimeMode, store.r.realtimeMode.value ? $style.on : null]" @click="toggleRealtimeMode">
<i class="ti ti-bolt ti-fw"></i> <i class="ti ti-bolt ti-fw"></i>

View File

@ -14,7 +14,7 @@ SPDX-License-Identifier: AGPL-3.0-only
mode="default" mode="default"
> >
<MkMarqueeText :key="key" :duration="marqueeDuration" :reverse="marqueeReverse"> <MkMarqueeText :key="key" :duration="marqueeDuration" :reverse="marqueeReverse">
<span v-for="instance in instances" :key="instance.id" :class="[$style.item, { [$style.colored]: colored }]" :style="{ background: colored ? instance.themeColor : null }"> <span v-for="instance in instances" :key="instance.id" :class="[$style.item, { [$style.colored]: colored }]" :style="{ background: colored ? instance.themeColor : '' }">
<img :class="$style.icon" :src="getInstanceIcon(instance)" alt=""/> <img :class="$style.icon" :src="getInstanceIcon(instance)" alt=""/>
<MkA :to="`/instance-info/${instance.host}`" :class="$style.host" class="_monospace"> <MkA :to="`/instance-info/${instance.host}`" :class="$style.host" class="_monospace">
{{ instance.host }} {{ instance.host }}
@ -33,9 +33,9 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup> <script lang="ts" setup>
import { ref } from 'vue'; import { ref } from 'vue';
import * as Misskey from 'misskey-js'; import * as Misskey from 'misskey-js';
import { useInterval } from '@@/js/use-interval.js';
import MkMarqueeText from '@/components/MkMarqueeText.vue'; import MkMarqueeText from '@/components/MkMarqueeText.vue';
import { misskeyApi } from '@/utility/misskey-api.js'; import { misskeyApi } from '@/utility/misskey-api.js';
import { useInterval } from '@@/js/use-interval.js';
import { getProxiedImageUrlNullable } from '@/utility/media-proxy.js'; import { getProxiedImageUrlNullable } from '@/utility/media-proxy.js';
const props = defineProps<{ const props = defineProps<{
@ -44,7 +44,7 @@ const props = defineProps<{
marqueeDuration?: number; marqueeDuration?: number;
marqueeReverse?: boolean; marqueeReverse?: boolean;
oneByOneInterval?: number; oneByOneInterval?: number;
refreshIntervalSec?: number; refreshIntervalSec: number;
}>(); }>();
const instances = ref<Misskey.entities.FederationInstance[]>([]); const instances = ref<Misskey.entities.FederationInstance[]>([]);

View File

@ -15,7 +15,7 @@ SPDX-License-Identifier: AGPL-3.0-only
> >
<MkMarqueeText :key="key" :duration="marqueeDuration" :reverse="marqueeReverse"> <MkMarqueeText :key="key" :duration="marqueeDuration" :reverse="marqueeReverse">
<span v-for="note in notes" :key="note.id" :class="$style.item"> <span v-for="note in notes" :key="note.id" :class="$style.item">
<img :class="$style.avatar" :src="note.user.avatarUrl" decoding="async"/> <img v-if="note.user.avatarUrl" :class="$style.avatar" :src="note.user.avatarUrl" decoding="async"/>
<MkA :class="$style.text" :to="notePage(note)"> <MkA :class="$style.text" :to="notePage(note)">
<Mfm :text="getNoteSummary(note)" :plain="true" :nowrap="true"/> <Mfm :text="getNoteSummary(note)" :plain="true" :nowrap="true"/>
</MkA> </MkA>
@ -33,9 +33,9 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup> <script lang="ts" setup>
import { ref, watch } from 'vue'; import { ref, watch } from 'vue';
import * as Misskey from 'misskey-js'; import * as Misskey from 'misskey-js';
import { useInterval } from '@@/js/use-interval.js';
import MkMarqueeText from '@/components/MkMarqueeText.vue'; import MkMarqueeText from '@/components/MkMarqueeText.vue';
import { misskeyApi } from '@/utility/misskey-api.js'; import { misskeyApi } from '@/utility/misskey-api.js';
import { useInterval } from '@@/js/use-interval.js';
import { getNoteSummary } from '@/utility/get-note-summary.js'; import { getNoteSummary } from '@/utility/get-note-summary.js';
import { notePage } from '@/filters/note.js'; import { notePage } from '@/filters/note.js';
@ -45,7 +45,7 @@ const props = defineProps<{
marqueeDuration?: number; marqueeDuration?: number;
marqueeReverse?: boolean; marqueeReverse?: boolean;
oneByOneInterval?: number; oneByOneInterval?: number;
refreshIntervalSec?: number; refreshIntervalSec: number;
}>(); }>();
const notes = ref<Misskey.entities.Note[]>([]); const notes = ref<Misskey.entities.Note[]>([]);

View File

@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template> <template>
<div :class="$style.root"> <div :class="$style.root">
<div :class="$style.title"> <div :class="$style.title">
<img :src="instance.iconUrl || instance.faviconUrl || '/favicon.ico'" alt="" :class="$style.instanceIcon"/> <img :src="instance.iconUrl || '/favicon.ico'" alt="" :class="$style.instanceIcon"/>
<span :class="$style.instanceTitle">{{ instance.name ?? host }}</span> <span :class="$style.instanceTitle">{{ instance.name ?? host }}</span>
</div> </div>
<div :class="$style.controls"> <div :class="$style.controls">