-
+
@@ -25,7 +25,9 @@ SPDX-License-Identifier: AGPL-3.0-only
diff --git a/packages/frontend/src/router/definition.ts b/packages/frontend/src/router/definition.ts
index f7a219c57e19..995a2055b850 100644
--- a/packages/frontend/src/router/definition.ts
+++ b/packages/frontend/src/router/definition.ts
@@ -232,6 +232,9 @@ const routes: RouteDef[] = [{
component: page(() => import('@/pages/search.vue')),
query: {
q: 'query',
+ userId: 'userId',
+ username: 'username',
+ host: 'host',
channel: 'channel',
type: 'type',
origin: 'origin',
diff --git a/packages/frontend/src/scripts/check-permissions.ts b/packages/frontend/src/scripts/check-permissions.ts
new file mode 100644
index 000000000000..ed86529d5b89
--- /dev/null
+++ b/packages/frontend/src/scripts/check-permissions.ts
@@ -0,0 +1,19 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { instance } from '@/instance.js';
+import { $i } from '@/account.js';
+
+export const notesSearchAvailable = (
+ // FIXME: instance.policies would be null in Vitest
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
+ ($i == null && instance.policies != null && instance.policies.canSearchNotes) ||
+ ($i != null && $i.policies.canSearchNotes) ||
+ false
+) as boolean;
+
+export const canSearchNonLocalNotes = (
+ instance.noteSearchableScope === 'global'
+);
diff --git a/packages/frontend/src/scripts/get-drive-file-menu.ts b/packages/frontend/src/scripts/get-drive-file-menu.ts
index 7c6c4b4db440..108648d640ad 100644
--- a/packages/frontend/src/scripts/get-drive-file-menu.ts
+++ b/packages/frontend/src/scripts/get-drive-file-menu.ts
@@ -41,6 +41,15 @@ function describe(file: Misskey.entities.DriveFile) {
});
}
+function move(file: Misskey.entities.DriveFile) {
+ os.selectDriveFolder(false).then(folder => {
+ misskeyApi('drive/files/update', {
+ fileId: file.id,
+ folderId: folder[0] ? folder[0].id : null,
+ });
+ });
+}
+
function toggleSensitive(file: Misskey.entities.DriveFile) {
misskeyApi('drive/files/update', {
fileId: file.id,
@@ -88,6 +97,10 @@ export function getDriveFileMenu(file: Misskey.entities.DriveFile, folder?: Miss
text: i18n.ts.rename,
icon: 'ti ti-forms',
action: () => rename(file),
+ }, {
+ text: i18n.ts.move,
+ icon: 'ti ti-folder-symlink',
+ action: () => move(file),
}, {
text: file.isSensitive ? i18n.ts.unmarkAsSensitive : i18n.ts.markAsSensitive,
icon: file.isSensitive ? 'ti ti-eye' : 'ti ti-eye-exclamation',
diff --git a/packages/frontend/src/scripts/get-user-menu.ts b/packages/frontend/src/scripts/get-user-menu.ts
index dacafb859f52..33f16a68aa61 100644
--- a/packages/frontend/src/scripts/get-user-menu.ts
+++ b/packages/frontend/src/scripts/get-user-menu.ts
@@ -13,9 +13,11 @@ import * as os from '@/os.js';
import { misskeyApi } from '@/scripts/misskey-api.js';
import { defaultStore, userActions } from '@/store.js';
import { $i, iAmModerator } from '@/account.js';
+import { notesSearchAvailable, canSearchNonLocalNotes } from '@/scripts/check-permissions.js';
import { IRouter } from '@/nirax.js';
import { antennasCache, rolesCache, userListsCache } from '@/cache.js';
import { mainRouter } from '@/router/main.js';
+import { MenuItem } from '@/types/menu.js';
export function getUserMenu(user: Misskey.entities.UserDetailed, router: IRouter = mainRouter) {
const meId = $i ? $i.id : null;
@@ -81,15 +83,6 @@ export function getUserMenu(user: Misskey.entities.UserDetailed, router: IRouter
});
}
- async function toggleWithReplies() {
- os.apiWithDialog('following/update', {
- userId: user.id,
- withReplies: !user.withReplies,
- }).then(() => {
- user.withReplies = !user.withReplies;
- });
- }
-
async function toggleNotify() {
os.apiWithDialog('following/update', {
userId: user.id,
@@ -154,13 +147,20 @@ export function getUserMenu(user: Misskey.entities.UserDetailed, router: IRouter
});
}
- let menu = [{
+ let menu: MenuItem[] = [{
icon: 'ti ti-at',
text: i18n.ts.copyUsername,
action: () => {
copyToClipboard(`@${user.username}@${user.host ?? host}`);
},
- }, ...(iAmModerator ? [{
+ }, ...( notesSearchAvailable && (user.host == null || canSearchNonLocalNotes) ? [{
+ icon: 'ti ti-search',
+ text: i18n.ts.searchThisUsersNotes,
+ action: () => {
+ router.push(`/search?username=${encodeURIComponent(user.username)}${user.host != null ? '&host=' + encodeURIComponent(user.host) : ''}`);
+ },
+ }] : [])
+ , ...(iAmModerator ? [{
icon: 'ti ti-user-exclamation',
text: i18n.ts.moderation,
action: () => {
@@ -306,15 +306,25 @@ export function getUserMenu(user: Misskey.entities.UserDetailed, router: IRouter
// フォローしたとしても user.isFollowing はリアルタイム更新されないので不便なため
//if (user.isFollowing) {
+ const withRepliesRef = ref(user.withReplies);
menu = menu.concat([{
- icon: user.withReplies ? 'ti ti-messages-off' : 'ti ti-messages',
- text: user.withReplies ? i18n.ts.hideRepliesToOthersInTimeline : i18n.ts.showRepliesToOthersInTimeline,
- action: toggleWithReplies,
+ type: 'switch',
+ icon: 'ti ti-messages',
+ text: i18n.ts.showRepliesToOthersInTimeline,
+ ref: withRepliesRef,
}, {
icon: user.notify === 'none' ? 'ti ti-bell' : 'ti ti-bell-off',
text: user.notify === 'none' ? i18n.ts.notifyNotes : i18n.ts.unnotifyNotes,
action: toggleNotify,
}]);
+ watch(withRepliesRef, (withReplies) => {
+ misskeyApi('following/update', {
+ userId: user.id,
+ withReplies,
+ }).then(() => {
+ user.withReplies = withReplies;
+ });
+ });
//}
menu = menu.concat([{ type: 'divider' }, {
diff --git a/packages/frontend/src/scripts/lookup.ts b/packages/frontend/src/scripts/lookup.ts
index 7f020b15cc2b..a261ec06691e 100644
--- a/packages/frontend/src/scripts/lookup.ts
+++ b/packages/frontend/src/scripts/lookup.ts
@@ -16,7 +16,7 @@ export async function lookup(router?: Router) {
title: i18n.ts.lookup,
});
const query = temp ? temp.trim() : '';
- if (canceled) return;
+ if (canceled || query.length <= 1) return;
if (query.startsWith('@') && !query.includes(' ')) {
_router.push(`/${query}`);
diff --git a/packages/frontend/src/scripts/merge.ts b/packages/frontend/src/scripts/merge.ts
index 4e39a0fa06ec..9794a300da02 100644
--- a/packages/frontend/src/scripts/merge.ts
+++ b/packages/frontend/src/scripts/merge.ts
@@ -6,7 +6,7 @@
import { deepClone } from './clone.js';
import type { Cloneable } from './clone.js';
-type DeepPartial = {
+export type DeepPartial = {
[P in keyof T]?: T[P] extends Record ? DeepPartial : T[P];
};
diff --git a/packages/frontend/src/scripts/sound.ts b/packages/frontend/src/scripts/sound.ts
index bba855cd6437..05f82fce7d27 100644
--- a/packages/frontend/src/scripts/sound.ts
+++ b/packages/frontend/src/scripts/sound.ts
@@ -124,10 +124,33 @@ export async function loadAudio(url: string, options?: { useCache?: boolean; })
*/
export function playMisskeySfx(operationType: OperationType) {
const sound = defaultStore.state[`sound_${operationType}`];
- if (sound.type == null || !canPlay || ('userActivation' in navigator && !navigator.userActivation.hasBeenActive)) return;
+ playMisskeySfxFile(sound).then((succeed) => {
+ if (!succeed && sound.type === '_driveFile_') {
+ // ドライブファイルが存在しない場合はデフォルトのサウンドを再生する
+ const soundName = defaultStore.def[`sound_${operationType}`].default.type as Exclude;
+ if (_DEV_) console.log(`Failed to play sound: ${sound.fileUrl}, so play default sound: ${soundName}`);
+ playMisskeySfxFileInternal({
+ type: soundName,
+ volume: sound.volume,
+ });
+ }
+ });
+}
+
+/**
+ * サウンド設定形式で指定された音声を再生する
+ * @param soundStore サウンド設定
+ */
+export async function playMisskeySfxFile(soundStore: SoundStore): Promise {
+ // 連続して再生しない
+ if (!canPlay) return false;
+ // ユーザーアクティベーションが必要な場合はそれがない場合は再生しない
+ if ('userActivation' in navigator && !navigator.userActivation.hasBeenActive) return false;
+ // サウンドがない場合は再生しない
+ if (soundStore.type === null || soundStore.type === '_driveFile_' && !soundStore.fileUrl) return false;
canPlay = false;
- playMisskeySfxFile(sound).finally(() => {
+ return await playMisskeySfxFileInternal(soundStore).finally(() => {
// ごく短時間に音が重複しないように
setTimeout(() => {
canPlay = true;
@@ -135,23 +158,22 @@ export function playMisskeySfx(operationType: OperationType) {
});
}
-/**
- * サウンド設定形式で指定された音声を再生する
- * @param soundStore サウンド設定
- */
-export async function playMisskeySfxFile(soundStore: SoundStore) {
+async function playMisskeySfxFileInternal(soundStore: SoundStore): Promise {
if (soundStore.type === null || (soundStore.type === '_driveFile_' && !soundStore.fileUrl)) {
- return;
+ return false;
}
const masterVolume = defaultStore.state.sound_masterVolume;
if (isMute() || masterVolume === 0 || soundStore.volume === 0) {
- return;
+ return true; // ミュート時は成功として扱う
}
const url = soundStore.type === '_driveFile_' ? soundStore.fileUrl : `/client-assets/sounds/${soundStore.type}.mp3`;
- const buffer = await loadAudio(url);
- if (!buffer) return;
+ const buffer = await loadAudio(url).catch(() => {
+ return undefined;
+ });
+ if (!buffer) return false;
const volume = soundStore.volume * masterVolume;
createSourceNode(buffer, { volume }).soundSource.start();
+ return true;
}
export async function playUrl(url: string, opts: {
diff --git a/packages/frontend/src/store.ts b/packages/frontend/src/store.ts
index dbf6b8716f91..437314074a0c 100644
--- a/packages/frontend/src/store.ts
+++ b/packages/frontend/src/store.ts
@@ -458,6 +458,10 @@ export const defaultStore = markRaw(new Storage('base', {
where: 'device',
default: false,
},
+ contextMenu: {
+ where: 'device',
+ default: 'app' as 'app' | 'appWithShift' | 'native',
+ },
sound_masterVolume: {
where: 'device',
diff --git a/packages/frontend/src/timelines.ts b/packages/frontend/src/timelines.ts
new file mode 100644
index 000000000000..94eda3545e1f
--- /dev/null
+++ b/packages/frontend/src/timelines.ts
@@ -0,0 +1,56 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { $i } from '@/account.js';
+import { instance } from '@/instance.js';
+
+export const basicTimelineTypes = [
+ 'home',
+ 'local',
+ 'social',
+ 'global',
+] as const;
+
+export type BasicTimelineType = typeof basicTimelineTypes[number];
+
+export function isBasicTimeline(timeline: string): timeline is BasicTimelineType {
+ return basicTimelineTypes.includes(timeline as BasicTimelineType);
+}
+
+export function basicTimelineIconClass(timeline: BasicTimelineType): string {
+ switch (timeline) {
+ case 'home':
+ return 'ti ti-home';
+ case 'local':
+ return 'ti ti-planet';
+ case 'social':
+ return 'ti ti-universe';
+ case 'global':
+ return 'ti ti-whirl';
+ }
+}
+
+export function isAvailableBasicTimeline(timeline: BasicTimelineType | undefined | null): boolean {
+ switch (timeline) {
+ case 'home':
+ return $i != null;
+ case 'local':
+ return ($i == null && instance.policies.ltlAvailable) || ($i != null && $i.policies.ltlAvailable);
+ case 'social':
+ return $i != null && $i.policies.ltlAvailable;
+ case 'global':
+ return ($i == null && instance.policies.gtlAvailable) || ($i != null && $i.policies.gtlAvailable);
+ default:
+ return false;
+ }
+}
+
+export function availableBasicTimelines(): BasicTimelineType[] {
+ return basicTimelineTypes.filter(isAvailableBasicTimeline);
+}
+
+export function hasWithReplies(timeline: BasicTimelineType | undefined | null): boolean {
+ return timeline === 'local' || timeline === 'social';
+}
diff --git a/packages/frontend/src/ui/deck.vue b/packages/frontend/src/ui/deck.vue
index bdb62dca15b5..af46b0641d83 100644
--- a/packages/frontend/src/ui/deck.vue
+++ b/packages/frontend/src/ui/deck.vue
@@ -24,7 +24,7 @@ SPDX-License-Identifier: AGPL-3.0-only
:ref="id"
:key="id"
:class="$style.column"
- :column="columns.find(c => c.id === id)"
+ :column="columns.find(c => c.id === id)!"
:isStacked="ids.length > 1"
@headerWheel="onWheel"
/>
@@ -95,7 +95,8 @@ SPDX-License-Identifier: AGPL-3.0-only
import { computed, defineAsyncComponent, ref, watch, shallowRef } from 'vue';
import { v4 as uuid } from 'uuid';
import XCommon from './_common_/common.vue';
-import { deckStore, addColumn as addColumnToStore, loadDeck, getProfiles, deleteProfile as deleteProfile_ } from './deck/deck-store.js';
+import { deckStore, columnTypes, addColumn as addColumnToStore, loadDeck, getProfiles, deleteProfile as deleteProfile_ } from './deck/deck-store.js';
+import type { ColumnType } from './deck/deck-store.js';
import XSidebar from '@/ui/_common_/navbar.vue';
import XDrawerMenu from '@/ui/_common_/navbar-for-mobile.vue';
import MkButton from '@/components/MkButton.vue';
@@ -152,10 +153,12 @@ window.addEventListener('resize', () => {
const snapScroll = deviceKind === 'smartphone' || deviceKind === 'tablet';
const drawerMenuShowing = ref(false);
+/*
const route = 'TODO';
watch(route, () => {
drawerMenuShowing.value = false;
});
+*/
const columns = deckStore.reactiveState.columns;
const layout = deckStore.reactiveState.layout;
@@ -174,32 +177,20 @@ function showSettings() {
const columnsEl = shallowRef();
const addColumn = async (ev) => {
- const columns = [
- 'main',
- 'widgets',
- 'notifications',
- 'tl',
- 'antenna',
- 'list',
- 'channel',
- 'mentions',
- 'direct',
- 'roleTimeline',
- ];
-
const { canceled, result: column } = await os.select({
title: i18n.ts._deck.addColumn,
- items: columns.map(column => ({
+ items: columnTypes.map(column => ({
value: column, text: i18n.ts._deck._columns[column],
})),
});
- if (canceled) return;
+ if (canceled || column == null) return;
addColumnToStore({
type: column,
id: uuid(),
name: i18n.ts._deck._columns[column],
width: 330,
+ soundSetting: { type: null, volume: 1 },
});
};
@@ -211,7 +202,7 @@ const onContextmenu = (ev) => {
};
function onWheel(ev: WheelEvent) {
- if (ev.deltaX === 0) {
+ if (ev.deltaX === 0 && columnsEl.value != null) {
columnsEl.value.scrollLeft += ev.deltaY;
}
}
@@ -242,7 +233,7 @@ function changeProfile(ev: MouseEvent) {
title: i18n.ts._deck.profile,
minLength: 1,
});
- if (canceled) return;
+ if (canceled || name == null) return;
deckStore.set('profile', name);
unisonReload();
diff --git a/packages/frontend/src/ui/deck/antenna-column.vue b/packages/frontend/src/ui/deck/antenna-column.vue
index c3dc1e4fcec3..987bd4db557e 100644
--- a/packages/frontend/src/ui/deck/antenna-column.vue
+++ b/packages/frontend/src/ui/deck/antenna-column.vue
@@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
-->
-
+
{{ column.name }}
@@ -14,7 +14,8 @@ SPDX-License-Identifier: AGPL-3.0-only