feat: チャンネルミュートの実装 (#14105)

* add channel_muting table and entities

* add channel_muting services

* タイムライン取得処理への組み込み

* misskey-jsの型とインターフェース生成

* Channelスキーマにミュート情報を追加

* フロントエンドの実装

* 条件が逆だったのを修正

* 期限切れミュートを掃除する機能を実装

* TLの抽出条件調節

* 名前の変更と変更不要の差分をロールバック

* 修正漏れ

* isChannelRelatedの条件に誤りがあった

* [wip] テスト追加

* テストの追加と検出した不備の修正

* fix test

* fix CHANGELOG.md

* 通常はFTTにしておく

* 実装忘れ対応

* fix merge

* fix merge

* add channel tl test

* fix CHANGELOG.md

* remove unused import

* fix lint

* fix test

* fix favorite -> favorited

* exclude -> include

* fix CHANGELOG.md

* fix CHANGELOG.md

* maintenance

* fix CHANGELOG.md

* fix

* fix ci

* regenerate

* fix

* Revert "fix"

This reverts commit 699d50c6ec798777d8e9667cb5d45a26b06bfc93.

* fixed

---------

Co-authored-by: syuilo <4439005+syuilo@users.noreply.github.com>
This commit is contained in:
おさむのひと
2025-11-07 08:39:21 +09:00
committed by GitHub
parent e74ab35de3
commit 729abbef62
53 changed files with 3564 additions and 151 deletions

View File

@@ -129,24 +129,25 @@ useInterval(() => {
});
watch(() => props.channelId, async () => {
channel.value = await misskeyApi('channels/show', {
const _channel = await misskeyApi('channels/show', {
channelId: props.channelId,
});
if (channel.value == null) return; // TSを黙らすため
favorited.value = channel.value.isFavorited ?? false;
if (favorited.value || channel.value.isFollowing) {
favorited.value = _channel.isFavorited ?? false;
if (favorited.value || _channel.isFollowing) {
tab.value = 'timeline';
}
if ((favorited.value || channel.value.isFollowing) && channel.value.lastNotedAt) {
const lastReadedAt: number = miLocalStorage.getItemAsJson(`channelLastReadedAt:${channel.value.id}`) ?? 0;
const lastNotedAt = Date.parse(channel.value.lastNotedAt);
if ((favorited.value || _channel.isFollowing) && _channel.lastNotedAt) {
const lastReadedAt: number = miLocalStorage.getItemAsJson(`channelLastReadedAt:${_channel.id}`) ?? 0;
const lastNotedAt = Date.parse(_channel.lastNotedAt);
if (lastNotedAt > lastReadedAt) {
miLocalStorage.setItemAsJson(`channelLastReadedAt:${channel.value.id}`, lastNotedAt);
miLocalStorage.setItemAsJson(`channelLastReadedAt:${_channel.id}`, lastNotedAt);
}
}
channel.value = _channel;
}, { immediate: true });
function edit() {
@@ -190,6 +191,53 @@ async function unfavorite() {
});
}
async function mute() {
if (!channel.value) return;
const _channel = channel.value;
const { canceled, result: period } = await os.select({
title: i18n.ts.mutePeriod,
items: [{
value: 'indefinitely', text: i18n.ts.indefinitely,
}, {
value: 'tenMinutes', text: i18n.ts.tenMinutes,
}, {
value: 'oneHour', text: i18n.ts.oneHour,
}, {
value: 'oneDay', text: i18n.ts.oneDay,
}, {
value: 'oneWeek', text: i18n.ts.oneWeek,
}],
default: 'indefinitely',
});
if (canceled) return;
const expiresAt = period === 'indefinitely' ? null
: period === 'tenMinutes' ? Date.now() + (1000 * 60 * 10)
: period === 'oneHour' ? Date.now() + (1000 * 60 * 60)
: period === 'oneDay' ? Date.now() + (1000 * 60 * 60 * 24)
: period === 'oneWeek' ? Date.now() + (1000 * 60 * 60 * 24 * 7)
: null;
os.apiWithDialog('channels/mute/create', {
channelId: _channel.id,
expiresAt,
}).then(() => {
_channel.isMuting = true;
});
}
async function unmute() {
if (!channel.value) return;
const _channel = channel.value;
os.apiWithDialog('channels/mute/delete', {
channelId: _channel.id,
}).then(() => {
_channel.isMuting = false;
});
}
async function search() {
if (!channel.value) return;
@@ -243,6 +291,24 @@ const headerActions = computed(() => {
});
}
if (!channel.value.isMuting) {
headerItems.push({
icon: 'ti ti-volume',
text: i18n.ts.mute,
handler: async (): Promise<void> => {
await mute();
},
});
} else {
headerItems.push({
icon: 'ti ti-volume-off',
text: i18n.ts.unmute,
handler: async (): Promise<void> => {
await unmute();
},
});
}
if (($i && $i.id === channel.value.userId) || iAmModerator) {
headerItems.push({
icon: 'ti ti-settings',