Compare commits
14 Commits
e60abb509a
...
7e06a6a2b7
Author | SHA1 | Date |
---|---|---|
|
7e06a6a2b7 | |
|
c4f8cda4aa | |
|
15c5e46e4f | |
|
42c53c830e | |
|
da58d43a7a | |
|
6e5b6a3bdb | |
|
3375619396 | |
|
beca66af01 | |
|
dee571ccde | |
|
451f0f7bd1 | |
|
295fe859d8 | |
|
287380b2db | |
|
834281c09c | |
|
e096841d35 |
|
@ -2322,6 +2322,10 @@ export interface Locale extends ILocale {
|
|||
* 新しいノートがあります
|
||||
*/
|
||||
"newNoteRecived": string;
|
||||
/**
|
||||
* 新しいノート
|
||||
*/
|
||||
"newNote": string;
|
||||
/**
|
||||
* サウンド
|
||||
*/
|
||||
|
@ -5717,6 +5721,22 @@ export interface Locale extends ILocale {
|
|||
* デバイス間でインストールしたテーマを同期
|
||||
*/
|
||||
"enableSyncThemesBetweenDevices": string;
|
||||
/**
|
||||
* サーバーと接続を確立し、リアルタイムでコンテンツを更新します。通信量とバッテリーの消費が多くなる場合があります。
|
||||
*/
|
||||
"realtimeMode_description": string;
|
||||
/**
|
||||
* コンテンツの取得頻度
|
||||
*/
|
||||
"contentsUpdateFrequency": string;
|
||||
/**
|
||||
* 高いほどリアルタイムにコンテンツが更新されますが、パフォーマンスが低下し、通信量とバッテリーの消費が多くなります。
|
||||
*/
|
||||
"contentsUpdateFrequency_description": string;
|
||||
/**
|
||||
* リアルタイムモードがオンのときは、この設定に関わらずリアルタイムでコンテンツが更新されます。
|
||||
*/
|
||||
"contentsUpdateFrequency_description2": string;
|
||||
"_chat": {
|
||||
/**
|
||||
* 送信者の名前を表示
|
||||
|
|
|
@ -576,6 +576,7 @@ showFixedPostForm: "タイムライン上部に投稿フォームを表示する
|
|||
showFixedPostFormInChannel: "タイムライン上部に投稿フォームを表示する(チャンネル)"
|
||||
withRepliesByDefaultForNewlyFollowed: "フォローする際、デフォルトで返信をTLに含むようにする"
|
||||
newNoteRecived: "新しいノートがあります"
|
||||
newNote: "新しいノート"
|
||||
sounds: "サウンド"
|
||||
sound: "サウンド"
|
||||
listen: "聴く"
|
||||
|
@ -1429,6 +1430,10 @@ _settings:
|
|||
ifOn: "オンのとき"
|
||||
ifOff: "オフのとき"
|
||||
enableSyncThemesBetweenDevices: "デバイス間でインストールしたテーマを同期"
|
||||
realtimeMode_description: "サーバーと接続を確立し、リアルタイムでコンテンツを更新します。通信量とバッテリーの消費が多くなる場合があります。"
|
||||
contentsUpdateFrequency: "コンテンツの取得頻度"
|
||||
contentsUpdateFrequency_description: "高いほどリアルタイムにコンテンツが更新されますが、パフォーマンスが低下し、通信量とバッテリーの消費が多くなります。"
|
||||
contentsUpdateFrequency_description2: "リアルタイムモードがオンのときは、この設定に関わらずリアルタイムでコンテンツが更新されます。"
|
||||
|
||||
_chat:
|
||||
showSenderName: "送信者の名前を表示"
|
||||
|
|
|
@ -415,11 +415,7 @@ provide(DI.mfmEmojiReactCallback, (reaction) => {
|
|||
});
|
||||
});
|
||||
|
||||
if (props.mock) {
|
||||
watch(() => props.note, (to) => {
|
||||
note = deepClone(to);
|
||||
}, { deep: true });
|
||||
} else {
|
||||
if (!props.mock) {
|
||||
useNoteCapture({
|
||||
note: appearNote,
|
||||
parentNote: note,
|
||||
|
@ -540,6 +536,9 @@ function react(): void {
|
|||
|
||||
if (props.mock) {
|
||||
emit('reaction', reaction);
|
||||
$appearNote.reactions[reaction] = 1;
|
||||
$appearNote.reactionCount++;
|
||||
$appearNote.myReaction = reaction;
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
@ -78,7 +78,12 @@ const paginator = usePagination({
|
|||
},
|
||||
});
|
||||
|
||||
const POLLING_INTERVAL = 1000 * 15;
|
||||
const MIN_POLLING_INTERVAL = 1000 * 10;
|
||||
const POLLING_INTERVAL =
|
||||
prefer.s.pollingInterval === 1 ? MIN_POLLING_INTERVAL * 1.5 * 1.5 :
|
||||
prefer.s.pollingInterval === 2 ? MIN_POLLING_INTERVAL * 1.5 :
|
||||
prefer.s.pollingInterval === 3 ? MIN_POLLING_INTERVAL :
|
||||
MIN_POLLING_INTERVAL;
|
||||
|
||||
if (!store.s.realtimeMode) {
|
||||
useInterval(async () => {
|
||||
|
|
|
@ -19,7 +19,11 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</div>
|
||||
|
||||
<div v-else ref="rootEl">
|
||||
<div v-if="paginator.queue.value.length > 0" :class="$style.new" @click="releaseQueue()"><button class="_button" :class="$style.newButton">{{ i18n.ts.newNoteRecived }}</button></div>
|
||||
<div v-if="paginator.queuedAheadItemsCount.value > 0" :class="$style.new">
|
||||
<div :class="$style.newBg1"></div>
|
||||
<div :class="$style.newBg2"></div>
|
||||
<button class="_button" :class="$style.newButton" @click="releaseQueue()"><i class="ti ti-circle-arrow-up"></i> {{ i18n.ts.newNote }}</button>
|
||||
</div>
|
||||
<component
|
||||
:is="prefer.s.animation ? TransitionGroup : 'div'"
|
||||
:class="[$style.notes, { [$style.noGap]: noGap, '_gaps': !noGap }]"
|
||||
|
@ -109,7 +113,12 @@ type TimelineQueryType = {
|
|||
|
||||
let adInsertionCounter = 0;
|
||||
|
||||
const POLLING_INTERVAL = 1000 * 15;
|
||||
const MIN_POLLING_INTERVAL = 1000 * 10;
|
||||
const POLLING_INTERVAL =
|
||||
prefer.s.pollingInterval === 1 ? MIN_POLLING_INTERVAL * 1.5 * 1.5 :
|
||||
prefer.s.pollingInterval === 2 ? MIN_POLLING_INTERVAL * 1.5 :
|
||||
prefer.s.pollingInterval === 3 ? MIN_POLLING_INTERVAL :
|
||||
MIN_POLLING_INTERVAL;
|
||||
|
||||
if (!store.s.realtimeMode) {
|
||||
useInterval(async () => {
|
||||
|
@ -125,11 +134,9 @@ if (!store.s.realtimeMode) {
|
|||
|
||||
globalEvents.on('notePosted', (note: Misskey.entities.Note) => {
|
||||
const isTop = rootEl.value == null ? false : isHeadVisible(rootEl.value, 16);
|
||||
if (isTop) {
|
||||
paginator.fetchNewer({
|
||||
toQueue: false,
|
||||
});
|
||||
}
|
||||
paginator.fetchNewer({
|
||||
toQueue: !isTop,
|
||||
});
|
||||
});
|
||||
|
||||
function releaseQueue() {
|
||||
|
@ -387,25 +394,77 @@ defineExpose({
|
|||
}
|
||||
|
||||
.new {
|
||||
--gapFill: 0.5px; // 上位ヘッダーの高さにフォントの関係などで少数が含まれると、レンダリングエンジンによっては隙間が表示されてしまうため、隙間を隠すために少しずらす
|
||||
|
||||
position: sticky;
|
||||
top: var(--MI-stickyTop, 0px);
|
||||
top: calc(var(--MI-stickyTop, 0px) - var(--gapFill));
|
||||
z-index: 1000;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
padding: 8px 0;
|
||||
padding: calc(10px + var(--gapFill)) 0 10px 0;
|
||||
}
|
||||
|
||||
/* 疑似progressive blur */
|
||||
.newBg1, .newBg2 {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
.newBg1 {
|
||||
height: 100%;
|
||||
-webkit-backdrop-filter: var(--MI-blur, blur(2px));
|
||||
backdrop-filter: var(--MI-blur, blur(2px));
|
||||
mask-image: linear-gradient( /* 疑似Easing Linear Gradients */
|
||||
to top,
|
||||
rgb(0 0 0 / 0%) 0%,
|
||||
rgb(0 0 0 / 4.9%) 7.75%,
|
||||
rgb(0 0 0 / 10.4%) 11.25%,
|
||||
rgb(0 0 0 / 45%) 23.55%,
|
||||
rgb(0 0 0 / 55%) 26.45%,
|
||||
rgb(0 0 0 / 89.6%) 38.75%,
|
||||
rgb(0 0 0 / 95.1%) 42.25%,
|
||||
rgb(0 0 0 / 100%) 50%
|
||||
);
|
||||
}
|
||||
|
||||
.newBg2 {
|
||||
height: 75%;
|
||||
-webkit-backdrop-filter: var(--MI-blur, blur(4px));
|
||||
backdrop-filter: var(--MI-blur, blur(4px));
|
||||
mask-image: linear-gradient( /* 疑似Easing Linear Gradients */
|
||||
to top,
|
||||
rgb(0 0 0 / 0%) 0%,
|
||||
rgb(0 0 0 / 4.9%) 15.5%,
|
||||
rgb(0 0 0 / 10.4%) 22.5%,
|
||||
rgb(0 0 0 / 45%) 47.1%,
|
||||
rgb(0 0 0 / 55%) 52.9%,
|
||||
rgb(0 0 0 / 89.6%) 77.5%,
|
||||
rgb(0 0 0 / 95.1%) 91.9%,
|
||||
rgb(0 0 0 / 100%) 100%
|
||||
);
|
||||
}
|
||||
|
||||
.newButton {
|
||||
position: relative;
|
||||
display: block;
|
||||
padding: 8px 16px;
|
||||
padding: 6px 12px;
|
||||
border-radius: 999px;
|
||||
width: max-content;
|
||||
margin: auto;
|
||||
background: var(--MI_THEME-accent);
|
||||
color: var(--MI_THEME-fgOnAccent);
|
||||
font-size: 90%;
|
||||
font-size: 85%;
|
||||
|
||||
&:hover {
|
||||
background: hsl(from var(--MI_THEME-accent) h s calc(l + 5));
|
||||
}
|
||||
|
||||
&:active {
|
||||
background: hsl(from var(--MI_THEME-accent) h s calc(l - 5));
|
||||
}
|
||||
}
|
||||
|
||||
.ad:empty {
|
||||
|
|
|
@ -76,8 +76,6 @@ const onceReacted = ref<boolean>(false);
|
|||
function addReaction(emoji) {
|
||||
onceReacted.value = true;
|
||||
emit('reacted');
|
||||
exampleNote.reactions[emoji] = 1;
|
||||
exampleNote.myReaction = emoji;
|
||||
doNotification(emoji);
|
||||
}
|
||||
|
||||
|
|
|
@ -41,6 +41,24 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</MkRadios>
|
||||
</SearchMarker>
|
||||
|
||||
<SearchMarker :keywords="['realtimemode']">
|
||||
<MkSwitch v-model="realtimeMode">
|
||||
<template #label><SearchLabel>{{ i18n.ts.realtimeMode }}</SearchLabel></template>
|
||||
<template #caption><SearchKeyword>{{ i18n.ts._settings.realtimeMode_description }}</SearchKeyword></template>
|
||||
</MkSwitch>
|
||||
</SearchMarker>
|
||||
|
||||
<MkDisableSection :disabled="realtimeMode">
|
||||
<SearchMarker :keywords="['polling', 'interval']">
|
||||
<MkPreferenceContainer k="pollingInterval">
|
||||
<MkRange v-model="pollingInterval" :min="1" :max="3" :step="1" easing :textConverter="(v) => v === 1 ? i18n.ts.low : v === 2 ? i18n.ts.middle : v === 3 ? i18n.ts.high : ''">
|
||||
<template #label><SearchLabel>{{ i18n.ts._settings.contentsUpdateFrequency }}</SearchLabel></template>
|
||||
<template #caption><SearchKeyword>{{ i18n.ts._settings.contentsUpdateFrequency_description }}</SearchKeyword><br><SearchKeyword>{{ i18n.ts._settings.contentsUpdateFrequency_description2 }}</SearchKeyword></template>
|
||||
</MkRange>
|
||||
</MkPreferenceContainer>
|
||||
</SearchMarker>
|
||||
</MkDisableSection>
|
||||
|
||||
<div class="_gaps_s">
|
||||
<SearchMarker :keywords="['titlebar', 'show']">
|
||||
<MkPreferenceContainer k="showTitlebar">
|
||||
|
@ -717,7 +735,7 @@ import MkRadios from '@/components/MkRadios.vue';
|
|||
import MkRange from '@/components/MkRange.vue';
|
||||
import MkFolder from '@/components/MkFolder.vue';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import FormSection from '@/components/form/section.vue';
|
||||
import MkDisableSection from '@/components/MkDisableSection.vue';
|
||||
import FormLink from '@/components/form/link.vue';
|
||||
import MkLink from '@/components/MkLink.vue';
|
||||
import MkInfo from '@/components/MkInfo.vue';
|
||||
|
@ -740,8 +758,10 @@ const $i = ensureSignin();
|
|||
|
||||
const lang = ref(miLocalStorage.getItem('lang'));
|
||||
const dataSaver = ref(prefer.s.dataSaver);
|
||||
const realtimeMode = computed(store.makeGetterSetter('realtimeMode'));
|
||||
|
||||
const overridedDeviceKind = prefer.model('overridedDeviceKind');
|
||||
const pollingInterval = prefer.model('pollingInterval');
|
||||
const showTitlebar = prefer.model('showTitlebar');
|
||||
const keepCw = prefer.model('keepCw');
|
||||
const serverDisconnectedBehavior = prefer.model('serverDisconnectedBehavior');
|
||||
|
@ -824,6 +844,8 @@ watch(useSystemFont, () => {
|
|||
watch([
|
||||
hemisphere,
|
||||
lang,
|
||||
realtimeMode,
|
||||
pollingInterval,
|
||||
enableInfiniteScroll,
|
||||
showNoteActionsOnlyHover,
|
||||
overridedDeviceKind,
|
||||
|
|
|
@ -241,6 +241,12 @@ export const PREF_DEF = {
|
|||
numberOfPageCache: {
|
||||
default: 3,
|
||||
},
|
||||
pollingInterval: {
|
||||
// 1 ... 低
|
||||
// 2 ... 中
|
||||
// 3 ... 高
|
||||
default: 2,
|
||||
},
|
||||
showNoteActionsOnlyHover: {
|
||||
default: false,
|
||||
},
|
||||
|
|
|
@ -11,6 +11,7 @@ import { useStream } from '@/stream.js';
|
|||
import { $i } from '@/i.js';
|
||||
import { store } from '@/store.js';
|
||||
import { misskeyApi } from '@/utility/misskey-api.js';
|
||||
import { prefer } from '@/preferences.js';
|
||||
|
||||
export const noteEvents = new EventEmitter<{
|
||||
[ev: `reacted:${string}`]: (ctx: { userId: Misskey.entities.User['id']; reaction: string; emoji?: { name: string; url: string; }; }) => void;
|
||||
|
@ -59,7 +60,12 @@ function pollingDequeue(note: Pick<Misskey.entities.Note, 'id' | 'createdAt'>) {
|
|||
}
|
||||
|
||||
const CAPTURE_MAX = 30;
|
||||
const POLLING_INTERVAL = 1000 * 15;
|
||||
const MIN_POLLING_INTERVAL = 1000 * 10;
|
||||
const POLLING_INTERVAL =
|
||||
prefer.s.pollingInterval === 1 ? MIN_POLLING_INTERVAL * 1.5 * 1.5 :
|
||||
prefer.s.pollingInterval === 2 ? MIN_POLLING_INTERVAL * 1.5 :
|
||||
prefer.s.pollingInterval === 3 ? MIN_POLLING_INTERVAL :
|
||||
MIN_POLLING_INTERVAL;
|
||||
|
||||
window.setInterval(() => {
|
||||
const ids = [...pollingQueue.entries()]
|
||||
|
|
|
@ -9,6 +9,7 @@ import type { ComputedRef, Ref, ShallowRef } from 'vue';
|
|||
import { misskeyApi } from '@/utility/misskey-api.js';
|
||||
|
||||
const MAX_ITEMS = 20;
|
||||
const MAX_QUEUE_ITEMS = 100;
|
||||
const FIRST_FETCH_LIMIT = 15;
|
||||
const SECOND_FETCH_LIMIT = 30;
|
||||
|
||||
|
@ -43,7 +44,8 @@ export function usePagination<T extends MisskeyEntity>(props: {
|
|||
useShallowRef?: boolean;
|
||||
}) {
|
||||
const items = props.useShallowRef ? shallowRef<T[]>([]) : ref<T[]>([]);
|
||||
const queue = props.useShallowRef ? shallowRef<T[]>([]) : ref<T[]>([]);
|
||||
let aheadQueue: T[] = [];
|
||||
const queuedAheadItemsCount = ref(0);
|
||||
const fetching = ref(true);
|
||||
const moreFetching = ref(false);
|
||||
const canFetchMore = ref(false);
|
||||
|
@ -54,6 +56,9 @@ export function usePagination<T extends MisskeyEntity>(props: {
|
|||
|
||||
function getNewestId() {
|
||||
// 様々な要因により並び順は保証されないのでソートが必要
|
||||
if (aheadQueue.length > 0) {
|
||||
return aheadQueue.map(x => x.id).sort().at(-1);
|
||||
}
|
||||
return items.value.map(x => x.id).sort().at(-1);
|
||||
}
|
||||
|
||||
|
@ -64,6 +69,8 @@ export function usePagination<T extends MisskeyEntity>(props: {
|
|||
|
||||
async function init(): Promise<void> {
|
||||
items.value = [];
|
||||
aheadQueue = [];
|
||||
queuedAheadItemsCount.value = 0;
|
||||
fetching.value = true;
|
||||
const params = props.ctx.params ? isRef(props.ctx.params) ? props.ctx.params.value : props.ctx.params : {};
|
||||
await misskeyApi<T[]>(props.ctx.endpoint, {
|
||||
|
@ -120,6 +127,7 @@ export function usePagination<T extends MisskeyEntity>(props: {
|
|||
moreFetching.value = false;
|
||||
} else {
|
||||
items.value.push(...res);
|
||||
if (props.useShallowRef) triggerRef(items);
|
||||
canFetchMore.value = true;
|
||||
moreFetching.value = false;
|
||||
}
|
||||
|
@ -142,8 +150,11 @@ export function usePagination<T extends MisskeyEntity>(props: {
|
|||
}),
|
||||
}).then(res => {
|
||||
if (options.toQueue) {
|
||||
queue.value.unshift(...res.toReversed());
|
||||
if (props.useShallowRef) triggerRef(queue);
|
||||
aheadQueue.unshift(...res.toReversed());
|
||||
if (aheadQueue.length > MAX_QUEUE_ITEMS) {
|
||||
aheadQueue = aheadQueue.slice(0, MAX_QUEUE_ITEMS);
|
||||
}
|
||||
queuedAheadItemsCount.value = aheadQueue.length;
|
||||
} else {
|
||||
items.value.unshift(...res.toReversed());
|
||||
if (props.useShallowRef) triggerRef(items);
|
||||
|
@ -172,13 +183,17 @@ export function usePagination<T extends MisskeyEntity>(props: {
|
|||
}
|
||||
|
||||
function enqueue(item: T) {
|
||||
queue.value.unshift(item);
|
||||
if (props.useShallowRef) triggerRef(queue);
|
||||
aheadQueue.unshift(item);
|
||||
if (aheadQueue.length > MAX_QUEUE_ITEMS) {
|
||||
aheadQueue.pop();
|
||||
}
|
||||
queuedAheadItemsCount.value = aheadQueue.length;
|
||||
}
|
||||
|
||||
function releaseQueue() {
|
||||
unshiftItems(queue.value);
|
||||
queue.value = [];
|
||||
unshiftItems(aheadQueue);
|
||||
aheadQueue = [];
|
||||
queuedAheadItemsCount.value = 0;
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
|
@ -187,7 +202,7 @@ export function usePagination<T extends MisskeyEntity>(props: {
|
|||
|
||||
return {
|
||||
items,
|
||||
queue,
|
||||
queuedAheadItemsCount,
|
||||
fetching,
|
||||
moreFetching,
|
||||
canFetchMore,
|
||||
|
|
Loading…
Reference in New Issue