新規にフォローした人のwithRepliesをtrueにする機能を追加 (#12048)

* feat: add defaultWithReplies to MiUser

* feat: use defaultWithReplies when creating MiFollowing

* feat: update defaultWithReplies from API

* feat: return defaultWithReplies as a part of $i

* feat(frontend): configure defaultWithReplies

* docs(changelog): 新規にフォローした人のをデフォルトでTL二追加できるように

* fix: typo

* style: fix lint failure

* chore: improve UI text

* chore: make optional params of  UserFollowingService.follow() object

* chore: UserFollowingService.follow() accept withReplies

* chore: add withReplies to MiFollowRequest

* chore: process withReplies for follow request

* feat: accept withReplies on 'following/create' endpoint

* feat: store defaultWithReplies in client store

* Revert "feat: return defaultWithReplies as a part of $i"

This reverts commit f2cc4fe6

* Revert "feat: update defaultWithReplies from API"

This reverts commit 95e3cee6

* Revert "feat: add defaultWithReplies to MiUser"

This reverts commit 9f5ab14d70.

* feat: configuring withReplies in import-following

* feat(frontend): configure withReplies

* fix(frontend): incorrectly showRepliesToOthersInTimeline can be shown

* fix(backend): withReplies of following/create not working

* fix(frontend): importFollowing error

* fix: withReplies is not working with follow import

* fix(frontend): use v-model

* style: fix lint

---------

Co-authored-by: Sayamame-beans <61457993+sayamame-beans@users.noreply.github.com>
Co-authored-by: syuilo <syuilotan@yahoo.co.jp>
This commit is contained in:
anatawa12 2023-10-17 20:56:17 +09:00 committed by GitHub
parent e9db0680c4
commit 5a3c6575dd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 105 additions and 23 deletions

View File

@ -18,6 +18,7 @@
- Feat: アンテナでローカルの投稿のみ収集できるようになりました - Feat: アンテナでローカルの投稿のみ収集できるようになりました
- Feat: サーバーサイレンス機能が追加されました - Feat: サーバーサイレンス機能が追加されました
- Enhance: 依存関係の更新 - Enhance: 依存関係の更新
- Enhance: 新規にフォローした人のをデフォルトでTLに追加できるように
### Client ### Client
- Enhance: TLの返信表示オプションを記憶するように - Enhance: TLの返信表示オプションを記憶するように

2
locales/index.d.ts vendored
View File

@ -537,6 +537,7 @@ export interface Locale {
"deleteAll": string; "deleteAll": string;
"showFixedPostForm": string; "showFixedPostForm": string;
"showFixedPostFormInChannel": string; "showFixedPostFormInChannel": string;
"withRepliesByDefaultForNewlyFollowed": string;
"newNoteRecived": string; "newNoteRecived": string;
"sounds": string; "sounds": string;
"sound": string; "sound": string;
@ -2054,6 +2055,7 @@ export interface Locale {
"userLists": string; "userLists": string;
"excludeMutingUsers": string; "excludeMutingUsers": string;
"excludeInactiveUsers": string; "excludeInactiveUsers": string;
"withReplies": string;
}; };
"_charts": { "_charts": {
"federation": string; "federation": string;

View File

@ -534,6 +534,7 @@ serverLogs: "サーバーログ"
deleteAll: "全て削除" deleteAll: "全て削除"
showFixedPostForm: "タイムライン上部に投稿フォームを表示する" showFixedPostForm: "タイムライン上部に投稿フォームを表示する"
showFixedPostFormInChannel: "タイムライン上部に投稿フォームを表示する(チャンネル)" showFixedPostFormInChannel: "タイムライン上部に投稿フォームを表示する(チャンネル)"
withRepliesByDefaultForNewlyFollowed: "フォローする際、デフォルトで返信をTLに含むようにする"
newNoteRecived: "新しいノートがあります" newNoteRecived: "新しいノートがあります"
sounds: "サウンド" sounds: "サウンド"
sound: "サウンド" sound: "サウンド"
@ -1969,6 +1970,7 @@ _exportOrImport:
userLists: "リスト" userLists: "リスト"
excludeMutingUsers: "ミュートしているユーザーを除外" excludeMutingUsers: "ミュートしているユーザーを除外"
excludeInactiveUsers: "使われていないアカウントを除外" excludeInactiveUsers: "使われていないアカウントを除外"
withReplies: "インポートした人による返信をTLに含むようにする"
_charts: _charts:
federation: "連合" federation: "連合"

View File

@ -0,0 +1,17 @@
/*
* SPDX-FileCopyrightText: syuilo and other misskey contributors
* SPDX-License-Identifier: AGPL-3.0-only
*/
export class FollowRequestWithReplies1697441463087 {
name = 'FollowRequestWithReplies1697441463087'
async up(queryRunner) {
await queryRunner.query(`ALTER TABLE "follow_request" ADD "withReplies" boolean NOT NULL DEFAULT false`);
}
async down(queryRunner) {
await queryRunner.query(`ALTER TABLE "follow_request" DROP COLUMN "withReplies"`);
}
}

View File

@ -237,10 +237,11 @@ export class QueueService {
} }
@bindThis @bindThis
public createImportFollowingJob(user: ThinUser, fileId: MiDriveFile['id']) { public createImportFollowingJob(user: ThinUser, fileId: MiDriveFile['id'], withReplies?: boolean) {
return this.dbQueue.add('importFollowing', { return this.dbQueue.add('importFollowing', {
user: { id: user.id }, user: { id: user.id },
fileId: fileId, fileId: fileId,
withReplies,
}, { }, {
removeOnComplete: true, removeOnComplete: true,
removeOnFail: true, removeOnFail: true,
@ -248,8 +249,8 @@ export class QueueService {
} }
@bindThis @bindThis
public createImportFollowingToDbJob(user: ThinUser, targets: string[]) { public createImportFollowingToDbJob(user: ThinUser, targets: string[], withReplies?: boolean) {
const jobs = targets.map(rel => this.generateToDbJobData('importFollowingToDb', { user, target: rel })); const jobs = targets.map(rel => this.generateToDbJobData('importFollowingToDb', { user, target: rel, withReplies }));
return this.dbQueue.addBulk(jobs); return this.dbQueue.addBulk(jobs);
} }
@ -342,7 +343,7 @@ export class QueueService {
} }
@bindThis @bindThis
public createFollowJob(followings: { from: ThinUser, to: ThinUser, requestId?: string, silent?: boolean }[]) { public createFollowJob(followings: { from: ThinUser, to: ThinUser, requestId?: string, silent?: boolean, withReplies?: boolean }[]) {
const jobs = followings.map(rel => this.generateRelationshipJobData('follow', rel)); const jobs = followings.map(rel => this.generateRelationshipJobData('follow', rel));
return this.relationshipQueue.addBulk(jobs); return this.relationshipQueue.addBulk(jobs);
} }
@ -384,6 +385,7 @@ export class QueueService {
to: { id: data.to.id }, to: { id: data.to.id },
silent: data.silent, silent: data.silent,
requestId: data.requestId, requestId: data.requestId,
withReplies: data.withReplies,
}, },
opts: { opts: {
removeOnComplete: true, removeOnComplete: true,

View File

@ -93,7 +93,15 @@ export class UserFollowingService implements OnModuleInit {
} }
@bindThis @bindThis
public async follow(_follower: { id: MiUser['id'] }, _followee: { id: MiUser['id'] }, requestId?: string, silent = false): Promise<void> { public async follow(
_follower: { id: MiUser['id'] },
_followee: { id: MiUser['id'] },
{ requestId, silent = false, withReplies }: {
requestId?: string,
silent?: boolean,
withReplies?: boolean,
} = {},
): Promise<void> {
const [follower, followee] = await Promise.all([ const [follower, followee] = await Promise.all([
this.usersRepository.findOneByOrFail({ id: _follower.id }), this.usersRepository.findOneByOrFail({ id: _follower.id }),
this.usersRepository.findOneByOrFail({ id: _followee.id }), this.usersRepository.findOneByOrFail({ id: _followee.id }),
@ -171,12 +179,12 @@ export class UserFollowingService implements OnModuleInit {
} }
if (!autoAccept) { if (!autoAccept) {
await this.createFollowRequest(follower, followee, requestId); await this.createFollowRequest(follower, followee, requestId, withReplies);
return; return;
} }
} }
await this.insertFollowingDoc(followee, follower, silent); await this.insertFollowingDoc(followee, follower, silent, withReplies);
if (this.userEntityService.isRemoteUser(follower) && this.userEntityService.isLocalUser(followee)) { if (this.userEntityService.isRemoteUser(follower) && this.userEntityService.isLocalUser(followee)) {
const content = this.apRendererService.addContext(this.apRendererService.renderAccept(this.apRendererService.renderFollow(follower, followee, requestId), followee)); const content = this.apRendererService.addContext(this.apRendererService.renderAccept(this.apRendererService.renderFollow(follower, followee, requestId), followee));
@ -193,6 +201,7 @@ export class UserFollowingService implements OnModuleInit {
id: MiUser['id']; host: MiUser['host']; uri: MiUser['host']; inbox: MiUser['inbox']; sharedInbox: MiUser['sharedInbox'] id: MiUser['id']; host: MiUser['host']; uri: MiUser['host']; inbox: MiUser['inbox']; sharedInbox: MiUser['sharedInbox']
}, },
silent = false, silent = false,
withReplies?: boolean,
): Promise<void> { ): Promise<void> {
if (follower.id === followee.id) return; if (follower.id === followee.id) return;
@ -202,6 +211,7 @@ export class UserFollowingService implements OnModuleInit {
id: this.idService.gen(), id: this.idService.gen(),
followerId: follower.id, followerId: follower.id,
followeeId: followee.id, followeeId: followee.id,
withReplies: withReplies,
// 非正規化 // 非正規化
followerHost: follower.host, followerHost: follower.host,
@ -454,6 +464,7 @@ export class UserFollowingService implements OnModuleInit {
id: MiUser['id']; host: MiUser['host']; uri: MiUser['host']; inbox: MiUser['inbox']; sharedInbox: MiUser['sharedInbox']; id: MiUser['id']; host: MiUser['host']; uri: MiUser['host']; inbox: MiUser['inbox']; sharedInbox: MiUser['sharedInbox'];
}, },
requestId?: string, requestId?: string,
withReplies?: boolean,
): Promise<void> { ): Promise<void> {
if (follower.id === followee.id) return; if (follower.id === followee.id) return;
@ -471,6 +482,7 @@ export class UserFollowingService implements OnModuleInit {
followerId: follower.id, followerId: follower.id,
followeeId: followee.id, followeeId: followee.id,
requestId, requestId,
withReplies,
// 非正規化 // 非正規化
followerHost: follower.host, followerHost: follower.host,
@ -555,7 +567,7 @@ export class UserFollowingService implements OnModuleInit {
throw new IdentifiableError('8884c2dd-5795-4ac9-b27e-6a01d38190f9', 'No follow request.'); throw new IdentifiableError('8884c2dd-5795-4ac9-b27e-6a01d38190f9', 'No follow request.');
} }
await this.insertFollowingDoc(followee, follower); await this.insertFollowingDoc(followee, follower, false, request.withReplies);
if (this.userEntityService.isRemoteUser(follower) && this.userEntityService.isLocalUser(followee)) { if (this.userEntityService.isRemoteUser(follower) && this.userEntityService.isLocalUser(followee)) {
const content = this.apRendererService.addContext(this.apRendererService.renderAccept(this.apRendererService.renderFollow(follower, followee as MiPartialLocalUser, request.requestId!), followee)); const content = this.apRendererService.addContext(this.apRendererService.renderAccept(this.apRendererService.renderFollow(follower, followee as MiPartialLocalUser, request.requestId!), followee));

View File

@ -164,7 +164,7 @@ export class ApInboxService {
} }
// don't queue because the sender may attempt again when timeout // don't queue because the sender may attempt again when timeout
await this.userFollowingService.follow(actor, followee, activity.id); await this.userFollowingService.follow(actor, followee, { requestId: activity.id });
return 'ok'; return 'ok';
} }

View File

@ -45,6 +45,11 @@ export class MiFollowRequest {
}) })
public requestId: string | null; public requestId: string | null;
@Column('boolean', {
default: false,
})
public withReplies: boolean;
//#region Denormalized fields //#region Denormalized fields
@Column('varchar', { @Column('varchar', {
length: 128, nullable: true, length: 128, nullable: true,

View File

@ -56,7 +56,7 @@ export class ImportFollowingProcessorService {
const csv = await this.downloadService.downloadTextFile(file.url); const csv = await this.downloadService.downloadTextFile(file.url);
const targets = csv.trim().split('\n'); const targets = csv.trim().split('\n');
this.queueService.createImportFollowingToDbJob({ id: user.id }, targets); this.queueService.createImportFollowingToDbJob({ id: user.id }, targets, job.data.withReplies);
this.logger.succ('Import jobs created'); this.logger.succ('Import jobs created');
} }
@ -93,9 +93,9 @@ export class ImportFollowingProcessorService {
// skip myself // skip myself
if (target.id === job.data.user.id) return; if (target.id === job.data.user.id) return;
this.logger.info(`Follow ${target.id} ...`); this.logger.info(`Follow ${target.id} ${job.data.withReplies ? 'with replies' : 'without replies'} ...`);
this.queueService.createFollowJob([{ from: user, to: { id: target.id }, silent: true }]); this.queueService.createFollowJob([{ from: user, to: { id: target.id }, silent: true, withReplies: job.data.withReplies }]);
} catch (e) { } catch (e) {
this.logger.warn(`Error: ${e}`); this.logger.warn(`Error: ${e}`);
} }

View File

@ -34,8 +34,12 @@ export class RelationshipProcessorService {
@bindThis @bindThis
public async processFollow(job: Bull.Job<RelationshipJobData>): Promise<string> { public async processFollow(job: Bull.Job<RelationshipJobData>): Promise<string> {
this.logger.info(`${job.data.from.id} is trying to follow ${job.data.to.id}`); this.logger.info(`${job.data.from.id} is trying to follow ${job.data.to.id} ${job.data.withReplies ? "with replies" : "without replies"}`);
await this.userFollowingService.follow(job.data.from, job.data.to, job.data.requestId, job.data.silent); await this.userFollowingService.follow(job.data.from, job.data.to, {
requestId: job.data.requestId,
silent: job.data.silent,
withReplies: job.data.withReplies,
});
return 'ok'; return 'ok';
} }

View File

@ -32,6 +32,7 @@ export type RelationshipJobData = {
to: ThinUser; to: ThinUser;
silent?: boolean; silent?: boolean;
requestId?: string; requestId?: string;
withReplies?: boolean;
} }
export type DbJobData<T extends keyof DbJobMap> = DbJobMap[T]; export type DbJobData<T extends keyof DbJobMap> = DbJobMap[T];
@ -79,6 +80,7 @@ export type DbUserDeleteJobData = {
export type DbUserImportJobData = { export type DbUserImportJobData = {
user: ThinUser; user: ThinUser;
fileId: MiDriveFile['id']; fileId: MiDriveFile['id'];
withReplies?: boolean;
}; };
export type DBAntennaImportJobData = { export type DBAntennaImportJobData = {
@ -89,6 +91,7 @@ export type DBAntennaImportJobData = {
export type DbUserImportToDbJobData = { export type DbUserImportToDbJobData = {
user: ThinUser; user: ThinUser;
target: string; target: string;
withReplies?: boolean;
}; };
export type ObjectStorageJobData = ObjectStorageFileJobData | Record<string, unknown>; export type ObjectStorageJobData = ObjectStorageFileJobData | Record<string, unknown>;

View File

@ -71,6 +71,7 @@ export const paramDef = {
type: 'object', type: 'object',
properties: { properties: {
userId: { type: 'string', format: 'misskey:id' }, userId: { type: 'string', format: 'misskey:id' },
withReplies: { type: 'boolean' }
}, },
required: ['userId'], required: ['userId'],
} as const; } as const;
@ -112,7 +113,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
} }
try { try {
await this.userFollowingService.follow(follower, followee); await this.userFollowingService.follow(follower, followee, { withReplies: ps.withReplies });
} catch (e) { } catch (e) {
if (e instanceof IdentifiableError) { if (e instanceof IdentifiableError) {
if (e.id === '710e8fb0-b8c3-4922-be49-d5d93d8e6a6e') throw new ApiError(meta.errors.blocking); if (e.id === '710e8fb0-b8c3-4922-be49-d5d93d8e6a6e') throw new ApiError(meta.errors.blocking);

View File

@ -52,6 +52,7 @@ export const paramDef = {
type: 'object', type: 'object',
properties: { properties: {
fileId: { type: 'string', format: 'misskey:id' }, fileId: { type: 'string', format: 'misskey:id' },
withReplies: { type: 'boolean' },
}, },
required: ['fileId'], required: ['fileId'],
} as const; } as const;
@ -79,7 +80,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
); );
if (checkMoving ? file.size > 32 * 1024 * 1024 : file.size > 64 * 1024) throw new ApiError(meta.errors.tooBigFile); if (checkMoving ? file.size > 32 * 1024 * 1024 : file.size > 64 * 1024) throw new ApiError(meta.errors.tooBigFile);
this.queueService.createImportFollowingJob(me, file.id); this.queueService.createImportFollowingJob(me, file.id, ps.withReplies);
}); });
} }
} }

View File

@ -42,6 +42,7 @@ import { useStream } from '@/stream.js';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
import { claimAchievement } from '@/scripts/achievements.js'; import { claimAchievement } from '@/scripts/achievements.js';
import { $i } from '@/account.js'; import { $i } from '@/account.js';
import { defaultStore } from "@/store.js";
const props = withDefaults(defineProps<{ const props = withDefaults(defineProps<{
user: Misskey.entities.UserDetailed, user: Misskey.entities.UserDetailed,
@ -52,6 +53,10 @@ const props = withDefaults(defineProps<{
large: false, large: false,
}); });
const emit = defineEmits<{
(_: 'update:user', value: Misskey.entities.UserDetailed): void
}>();
let isFollowing = $ref(props.user.isFollowing); let isFollowing = $ref(props.user.isFollowing);
let hasPendingFollowRequestFromYou = $ref(props.user.hasPendingFollowRequestFromYou); let hasPendingFollowRequestFromYou = $ref(props.user.hasPendingFollowRequestFromYou);
let wait = $ref(false); let wait = $ref(false);
@ -95,6 +100,11 @@ async function onClick() {
} else { } else {
await os.api('following/create', { await os.api('following/create', {
userId: props.user.id, userId: props.user.id,
withReplies: defaultStore.state.defaultWithReplies,
});
emit('update:user', {
...props.user,
withReplies: defaultStore.state.defaultWithReplies
}); });
hasPendingFollowRequestFromYou = true; hasPendingFollowRequestFromYou = true;

View File

@ -45,7 +45,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</div> </div>
</div> </div>
<button class="_button" :class="$style.menu" @click="showMenu"><i class="ti ti-dots"></i></button> <button class="_button" :class="$style.menu" @click="showMenu"><i class="ti ti-dots"></i></button>
<MkFollowButton v-if="$i && user.id != $i.id" :class="$style.follow" :user="user" mini/> <MkFollowButton v-if="$i && user.id != $i.id" v-model:user="user" :class="$style.follow" mini/>
</div> </div>
<div v-else> <div v-else>
<MkLoading/> <MkLoading/>

View File

@ -14,6 +14,7 @@ import * as Misskey from 'misskey-js';
import * as os from '@/os.js'; import * as os from '@/os.js';
import { mainRouter } from '@/router.js'; import { mainRouter } from '@/router.js';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
import { defaultStore } from "@/store.js";
async function follow(user): Promise<void> { async function follow(user): Promise<void> {
const { canceled } = await os.confirm({ const { canceled } = await os.confirm({
@ -28,7 +29,9 @@ async function follow(user): Promise<void> {
os.apiWithDialog('following/create', { os.apiWithDialog('following/create', {
userId: user.id, userId: user.id,
withReplies: defaultStore.state.defaultWithReplies,
}); });
user.withReplies = defaultStore.state.defaultWithReplies;
} }
const acct = new URL(location.href).searchParams.get('acct'); const acct = new URL(location.href).searchParams.get('acct');

View File

@ -38,7 +38,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkUserName :user="post.user" style="display: block;"/> <MkUserName :user="post.user" style="display: block;"/>
<MkAcct :user="post.user"/> <MkAcct :user="post.user"/>
</div> </div>
<MkFollowButton v-if="!$i || $i.id != post.user.id" :user="post.user" :inline="true" :transparent="false" :full="true" large class="koudoku"/> <MkFollowButton v-if="!$i || $i.id != post.user.id" v-model:user="post.user" :inline="true" :transparent="false" :full="true" large class="koudoku"/>
</div> </div>
</div> </div>
<MkAd :prefer="['horizontal', 'horizontal-big']"/> <MkAd :prefer="['horizontal', 'horizontal-big']"/>

View File

@ -29,6 +29,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<div class="_gaps_s"> <div class="_gaps_s">
<MkSwitch v-model="showFixedPostForm">{{ i18n.ts.showFixedPostForm }}</MkSwitch> <MkSwitch v-model="showFixedPostForm">{{ i18n.ts.showFixedPostForm }}</MkSwitch>
<MkSwitch v-model="showFixedPostFormInChannel">{{ i18n.ts.showFixedPostFormInChannel }}</MkSwitch> <MkSwitch v-model="showFixedPostFormInChannel">{{ i18n.ts.showFixedPostFormInChannel }}</MkSwitch>
<MkSwitch v-model="defaultWithReplies">{{ i18n.ts.withRepliesByDefaultForNewlyFollowed }}</MkSwitch>
<MkFolder> <MkFolder>
<template #label>{{ i18n.ts.pinnedList }}</template> <template #label>{{ i18n.ts.pinnedList }}</template>
<!-- 複数ピン止め管理できるようにしたいけどめんどいので一旦ひとつのみ --> <!-- 複数ピン止め管理できるようにしたいけどめんどいので一旦ひとつのみ -->
@ -249,6 +250,7 @@ const mediaListWithOneImageAppearance = computed(defaultStore.makeGetterSetter('
const notificationPosition = computed(defaultStore.makeGetterSetter('notificationPosition')); const notificationPosition = computed(defaultStore.makeGetterSetter('notificationPosition'));
const notificationStackAxis = computed(defaultStore.makeGetterSetter('notificationStackAxis')); const notificationStackAxis = computed(defaultStore.makeGetterSetter('notificationStackAxis'));
const keepScreenOn = computed(defaultStore.makeGetterSetter('keepScreenOn')); const keepScreenOn = computed(defaultStore.makeGetterSetter('keepScreenOn'));
const defaultWithReplies = computed(defaultStore.makeGetterSetter('defaultWithReplies'));
watch(lang, () => { watch(lang, () => {
miLocalStorage.setItem('lang', lang.value as string); miLocalStorage.setItem('lang', lang.value as string);

View File

@ -40,6 +40,9 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkFolder v-if="$i && !$i.movedTo"> <MkFolder v-if="$i && !$i.movedTo">
<template #label>{{ i18n.ts.import }}</template> <template #label>{{ i18n.ts.import }}</template>
<template #icon><i class="ti ti-upload"></i></template> <template #icon><i class="ti ti-upload"></i></template>
<MkSwitch v-model="withReplies">
{{ i18n.ts._exportOrImport.withReplies }}
</MkSwitch>
<MkButton primary :class="$style.button" inline @click="importFollowing($event)"><i class="ti ti-upload"></i> {{ i18n.ts.import }}</MkButton> <MkButton primary :class="$style.button" inline @click="importFollowing($event)"><i class="ti ti-upload"></i> {{ i18n.ts.import }}</MkButton>
</MkFolder> </MkFolder>
</div> </div>
@ -118,9 +121,11 @@ import { selectFile } from '@/scripts/select-file.js';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
import { definePageMetadata } from '@/scripts/page-metadata.js'; import { definePageMetadata } from '@/scripts/page-metadata.js';
import { $i } from '@/account.js'; import { $i } from '@/account.js';
import { defaultStore } from "@/store.js";
const excludeMutingUsers = ref(false); const excludeMutingUsers = ref(false);
const excludeInactiveUsers = ref(false); const excludeInactiveUsers = ref(false);
const withReplies = ref(defaultStore.state.defaultWithReplies);
const onExportSuccess = () => { const onExportSuccess = () => {
os.alert({ os.alert({
@ -177,7 +182,10 @@ const exportAntennas = () => {
const importFollowing = async (ev) => { const importFollowing = async (ev) => {
const file = await selectFile(ev.currentTarget ?? ev.target); const file = await selectFile(ev.currentTarget ?? ev.target);
os.api('i/import-following', { fileId: file.id }).then(onImportSuccess).catch(onError); os.api('i/import-following', {
fileId: file.id,
withReplies: withReplies.value,
}).then(onImportSuccess).catch(onError);
}; };
const importUserLists = async (ev) => { const importUserLists = async (ev) => {

View File

@ -34,7 +34,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<span v-if="$i && $i.id != user.id && user.isFollowed" class="followed">{{ i18n.ts.followsYou }}</span> <span v-if="$i && $i.id != user.id && user.isFollowed" class="followed">{{ i18n.ts.followsYou }}</span>
<div v-if="$i" class="actions"> <div v-if="$i" class="actions">
<button class="menu _button" @click="menu"><i class="ti ti-dots"></i></button> <button class="menu _button" @click="menu"><i class="ti ti-dots"></i></button>
<MkFollowButton v-if="$i.id != user.id" :user="user" :inline="true" :transparent="false" :full="true" class="koudoku"/> <MkFollowButton v-if="$i.id != user.id" v-model:user="user" :inline="true" :transparent="false" :full="true" class="koudoku"/>
</div> </div>
</div> </div>
<MkAvatar class="avatar" :user="user" indicator/> <MkAvatar class="avatar" :user="user" indicator/>
@ -198,6 +198,7 @@ const props = withDefaults(defineProps<{
const router = useRouter(); const router = useRouter();
let user = $ref(props.user);
let parallaxAnimationId = $ref<null | number>(null); let parallaxAnimationId = $ref<null | number>(null);
let narrow = $ref<null | boolean>(null); let narrow = $ref<null | boolean>(null);
let rootEl = $ref<null | HTMLElement>(null); let rootEl = $ref<null | HTMLElement>(null);
@ -232,7 +233,7 @@ const age = $computed(() => {
}); });
function menu(ev) { function menu(ev) {
const { menu, cleanup } = getUserMenu(props.user, router); const { menu, cleanup } = getUserMenu(user, router);
os.popupMenu(menu, ev.currentTarget ?? ev.target).finally(cleanup); os.popupMenu(menu, ev.currentTarget ?? ev.target).finally(cleanup);
} }

View File

@ -361,6 +361,10 @@ export const defaultStore = markRaw(new Storage('base', {
where: 'device', where: 'device',
default: false, default: false,
}, },
defaultWithReplies: {
where: 'account',
default: false,
},
})); }));
// TODO: 他のタブと永続化されたstateを同期 // TODO: 他のタブと永続化されたstateを同期

View File

@ -1185,6 +1185,7 @@ export type Endpoints = {
'following/create': { 'following/create': {
req: { req: {
userId: User['id']; userId: User['id'];
withReplies?: boolean;
}; };
res: User; res: User;
}; };
@ -2985,7 +2986,7 @@ type UserSorting = '+follower' | '-follower' | '+createdAt' | '-createdAt' | '+u
// //
// src/api.types.ts:16:32 - (ae-forgotten-export) The symbol "TODO" needs to be exported by the entry point index.d.ts // src/api.types.ts:16:32 - (ae-forgotten-export) The symbol "TODO" needs to be exported by the entry point index.d.ts
// src/api.types.ts:18:25 - (ae-forgotten-export) The symbol "NoParams" needs to be exported by the entry point index.d.ts // src/api.types.ts:18:25 - (ae-forgotten-export) The symbol "NoParams" needs to be exported by the entry point index.d.ts
// src/api.types.ts:630:18 - (ae-forgotten-export) The symbol "ShowUserReq" needs to be exported by the entry point index.d.ts // src/api.types.ts:633:18 - (ae-forgotten-export) The symbol "ShowUserReq" needs to be exported by the entry point index.d.ts
// src/entities.ts:107:2 - (ae-forgotten-export) The symbol "notificationTypes_2" needs to be exported by the entry point index.d.ts // src/entities.ts:107:2 - (ae-forgotten-export) The symbol "notificationTypes_2" needs to be exported by the entry point index.d.ts
// src/entities.ts:603:2 - (ae-forgotten-export) The symbol "ModerationLogPayloads" needs to be exported by the entry point index.d.ts // src/entities.ts:603:2 - (ae-forgotten-export) The symbol "ModerationLogPayloads" needs to be exported by the entry point index.d.ts
// src/streaming.types.ts:33:4 - (ae-forgotten-export) The symbol "FIXME" needs to be exported by the entry point index.d.ts // src/streaming.types.ts:33:4 - (ae-forgotten-export) The symbol "FIXME" needs to be exported by the entry point index.d.ts

View File

@ -321,7 +321,10 @@ export type Endpoints = {
'federation/users': { req: { host: string; limit?: number; sinceId?: User['id']; untilId?: User['id']; }; res: UserDetailed[]; }; 'federation/users': { req: { host: string; limit?: number; sinceId?: User['id']; untilId?: User['id']; }; res: UserDetailed[]; };
// following // following
'following/create': { req: { userId: User['id'] }; res: User; }; 'following/create': { req: {
userId: User['id'],
withReplies?: boolean,
}; res: User; };
'following/delete': { req: { userId: User['id'] }; res: User; }; 'following/delete': { req: { userId: User['id'] }; res: User; };
'following/requests/accept': { req: { userId: User['id'] }; res: null; }; 'following/requests/accept': { req: { userId: User['id'] }; res: null; };
'following/requests/cancel': { req: { userId: User['id'] }; res: User; }; 'following/requests/cancel': { req: { userId: User['id'] }; res: User; };