Compare commits
25 commits
0da17148f9
...
afda33c0f4
Author | SHA1 | Date | |
---|---|---|---|
afda33c0f4 | |||
|
cace4153e4 | ||
|
f8b2e272f1 | ||
|
835e76152e | ||
|
c0c41af5f9 | ||
|
4430c12e0e | ||
|
1eb57201b4 | ||
|
0f68914610 | ||
|
d3a9995d0a | ||
|
c6ef944fc6 | ||
|
f6796a99ec | ||
|
32e2a07d66 | ||
|
b4bc58ae4c | ||
|
32b860c352 | ||
|
9dbdb97bb5 | ||
|
f402fd3313 | ||
|
0e4b7c91f1 | ||
|
0a0f3c3387 | ||
|
0cdb8e5b80 | ||
|
a46887d05f | ||
|
42d4fc9d97 | ||
|
da769846eb | ||
|
7f3dc6066d | ||
|
40a73bfcbe | ||
|
9f5c279478 |
28 changed files with 548 additions and 137 deletions
60
locales/index.d.ts
vendored
60
locales/index.d.ts
vendored
|
@ -7689,7 +7689,43 @@ export interface Locale extends ILocale {
|
|||
* Match subdomains
|
||||
*/
|
||||
"isFromInstanceSubdomains": string;
|
||||
/**
|
||||
* Has X or fewer local followers
|
||||
*/
|
||||
"localFollowersLessThanOrEq": string;
|
||||
/**
|
||||
* Has X or more local followers
|
||||
*/
|
||||
"localFollowersMoreThanOrEq": string;
|
||||
/**
|
||||
* Follows X or fewer local accounts
|
||||
*/
|
||||
"localFollowingLessThanOrEq": string;
|
||||
/**
|
||||
* Follows X or more local accounts
|
||||
*/
|
||||
"localFollowingMoreThanOrEq": string;
|
||||
/**
|
||||
* Has X or fewer remote followers
|
||||
*/
|
||||
"remoteFollowersLessThanOrEq": string;
|
||||
/**
|
||||
* Has X or more remote followers
|
||||
*/
|
||||
"remoteFollowersMoreThanOrEq": string;
|
||||
/**
|
||||
* Follows X or fewer remote accounts
|
||||
*/
|
||||
"remoteFollowingLessThanOrEq": string;
|
||||
/**
|
||||
* Follows X or more remote accounts
|
||||
*/
|
||||
"remoteFollowingMoreThanOrEq": string;
|
||||
};
|
||||
/**
|
||||
* This condition may be incorrect for remote users.
|
||||
*/
|
||||
"remoteDataWarning": string;
|
||||
};
|
||||
"_sensitiveMediaDetection": {
|
||||
/**
|
||||
|
@ -12969,6 +13005,30 @@ export interface Locale extends ILocale {
|
|||
*/
|
||||
"text": string;
|
||||
};
|
||||
/**
|
||||
* Test patterns
|
||||
*/
|
||||
"wordMuteTestLabel": string;
|
||||
/**
|
||||
* Enter some text here to test your word patterns. The matched words, if any, will be displayed below.
|
||||
*/
|
||||
"wordMuteTestDescription": string;
|
||||
/**
|
||||
* Test
|
||||
*/
|
||||
"wordMuteTestTest": string;
|
||||
/**
|
||||
* Matched words: {words}
|
||||
*/
|
||||
"wordMuteTestMatch": ParameterizedString<"words">;
|
||||
/**
|
||||
* No results yet, enter some text and click "Test" to check it.
|
||||
*/
|
||||
"wordMuteTestNoResults": string;
|
||||
/**
|
||||
* Text does not match any patterns.
|
||||
*/
|
||||
"wordMuteTestNoMatch": string;
|
||||
}
|
||||
declare const locales: {
|
||||
[lang: string]: Locale;
|
||||
|
|
|
@ -15,6 +15,13 @@ import { bindThis } from '@/decorators.js';
|
|||
import type { GlobalEvents } from '@/core/GlobalEventService.js';
|
||||
import type { OnApplicationShutdown } from '@nestjs/common';
|
||||
|
||||
export interface FollowStats {
|
||||
localFollowing: number;
|
||||
localFollowers: number;
|
||||
remoteFollowing: number;
|
||||
remoteFollowers: number;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class CacheService implements OnApplicationShutdown {
|
||||
public userByIdCache: MemoryKVCache<MiUser>;
|
||||
|
@ -27,6 +34,7 @@ export class CacheService implements OnApplicationShutdown {
|
|||
public userBlockedCache: RedisKVCache<Set<string>>; // NOTE: 「被」Blockキャッシュ
|
||||
public renoteMutingsCache: RedisKVCache<Set<string>>;
|
||||
public userFollowingsCache: RedisKVCache<Record<string, Pick<MiFollowing, 'withReplies'> | undefined>>;
|
||||
private readonly userFollowStatsCache = new MemoryKVCache<FollowStats>(1000 * 60 * 10); // 10 minutes
|
||||
|
||||
constructor(
|
||||
@Inject(DI.redis)
|
||||
|
@ -167,6 +175,18 @@ export class CacheService implements OnApplicationShutdown {
|
|||
const followee = this.userByIdCache.get(body.followeeId);
|
||||
if (followee) followee.followersCount++;
|
||||
this.userFollowingsCache.delete(body.followerId);
|
||||
this.userFollowStatsCache.delete(body.followerId);
|
||||
this.userFollowStatsCache.delete(body.followeeId);
|
||||
break;
|
||||
}
|
||||
case 'unfollow': {
|
||||
const follower = this.userByIdCache.get(body.followerId);
|
||||
if (follower) follower.followingCount--;
|
||||
const followee = this.userByIdCache.get(body.followeeId);
|
||||
if (followee) followee.followersCount--;
|
||||
this.userFollowingsCache.delete(body.followerId);
|
||||
this.userFollowStatsCache.delete(body.followerId);
|
||||
this.userFollowStatsCache.delete(body.followeeId);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
|
@ -187,6 +207,52 @@ export class CacheService implements OnApplicationShutdown {
|
|||
}) ?? null;
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async getFollowStats(userId: MiUser['id']): Promise<FollowStats> {
|
||||
return await this.userFollowStatsCache.fetch(userId, async () => {
|
||||
const stats = {
|
||||
localFollowing: 0,
|
||||
localFollowers: 0,
|
||||
remoteFollowing: 0,
|
||||
remoteFollowers: 0,
|
||||
};
|
||||
|
||||
const followings = await this.followingsRepository.findBy([
|
||||
{ followerId: userId },
|
||||
{ followeeId: userId },
|
||||
]);
|
||||
|
||||
for (const following of followings) {
|
||||
if (following.followerId === userId) {
|
||||
// increment following; user is a follower of someone else
|
||||
if (following.followeeHost == null) {
|
||||
stats.localFollowing++;
|
||||
} else {
|
||||
stats.remoteFollowing++;
|
||||
}
|
||||
} else if (following.followeeId === userId) {
|
||||
// increment followers; user is followed by someone else
|
||||
if (following.followerHost == null) {
|
||||
stats.localFollowers++;
|
||||
} else {
|
||||
stats.remoteFollowers++;
|
||||
}
|
||||
} else {
|
||||
// Should never happen
|
||||
}
|
||||
}
|
||||
|
||||
// Infer remote-remote followers heuristically, since we don't track that info directly.
|
||||
const user = await this.findUserById(userId);
|
||||
if (user.host !== null) {
|
||||
stats.remoteFollowing = Math.max(0, user.followingCount - stats.localFollowing);
|
||||
stats.remoteFollowers = Math.max(0, user.followersCount - stats.localFollowers);
|
||||
}
|
||||
|
||||
return stats;
|
||||
});
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public dispose(): void {
|
||||
this.redisForSub.off('message', this.onMessage);
|
||||
|
|
|
@ -10,7 +10,7 @@ import { IdentifiableError } from '@/misc/identifiable-error.js';
|
|||
import type { MiUser } from '@/models/User.js';
|
||||
import type { MiNote } from '@/models/Note.js';
|
||||
import { IdService } from '@/core/IdService.js';
|
||||
import type { MiUserNotePining } from '@/models/UserNotePining.js';
|
||||
import { MiUserNotePining } from '@/models/UserNotePining.js';
|
||||
import { RelayService } from '@/core/RelayService.js';
|
||||
import type { Config } from '@/config.js';
|
||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||
|
@ -18,6 +18,7 @@ import { ApDeliverManagerService } from '@/core/activitypub/ApDeliverManagerServ
|
|||
import { ApRendererService } from '@/core/activitypub/ApRendererService.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { RoleService } from '@/core/RoleService.js';
|
||||
import type { DataSource } from 'typeorm';
|
||||
|
||||
@Injectable()
|
||||
export class NotePiningService {
|
||||
|
@ -34,6 +35,9 @@ export class NotePiningService {
|
|||
@Inject(DI.userNotePiningsRepository)
|
||||
private userNotePiningsRepository: UserNotePiningsRepository,
|
||||
|
||||
@Inject(DI.db)
|
||||
private readonly db: DataSource,
|
||||
|
||||
private userEntityService: UserEntityService,
|
||||
private idService: IdService,
|
||||
private roleService: RoleService,
|
||||
|
@ -60,21 +64,23 @@ export class NotePiningService {
|
|||
throw new IdentifiableError('70c4e51f-5bea-449c-a030-53bee3cce202', 'No such note.');
|
||||
}
|
||||
|
||||
const pinings = await this.userNotePiningsRepository.findBy({ userId: user.id });
|
||||
await this.db.transaction(async tem => {
|
||||
const pinings = await tem.findBy(MiUserNotePining, { userId: user.id });
|
||||
|
||||
if (pinings.length >= (await this.roleService.getUserPolicies(user.id)).pinLimit) {
|
||||
throw new IdentifiableError('15a018eb-58e5-4da1-93be-330fcc5e4e1a', 'You can not pin notes any more.');
|
||||
}
|
||||
if (pinings.length >= (await this.roleService.getUserPolicies(user.id)).pinLimit) {
|
||||
throw new IdentifiableError('15a018eb-58e5-4da1-93be-330fcc5e4e1a', 'You can not pin notes any more.');
|
||||
}
|
||||
|
||||
if (pinings.some(pining => pining.noteId === note.id)) {
|
||||
throw new IdentifiableError('23f0cf4e-59a3-4276-a91d-61a5891c1514', 'That note has already been pinned.');
|
||||
}
|
||||
if (pinings.some(pining => pining.noteId === note.id)) {
|
||||
throw new IdentifiableError('23f0cf4e-59a3-4276-a91d-61a5891c1514', 'That note has already been pinned.');
|
||||
}
|
||||
|
||||
await this.userNotePiningsRepository.insert({
|
||||
id: this.idService.gen(),
|
||||
userId: user.id,
|
||||
noteId: note.id,
|
||||
} as MiUserNotePining);
|
||||
await tem.insert(MiUserNotePining, {
|
||||
id: this.idService.gen(),
|
||||
userId: user.id,
|
||||
noteId: note.id,
|
||||
});
|
||||
});
|
||||
|
||||
// Deliver to remote followers
|
||||
if (this.userEntityService.isLocalUser(user) && !note.localOnly && ['public', 'home'].includes(note.visibility)) {
|
||||
|
|
|
@ -20,6 +20,7 @@ import type { MiUser } from '@/models/User.js';
|
|||
import { DI } from '@/di-symbols.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { CacheService } from '@/core/CacheService.js';
|
||||
import type { FollowStats } from '@/core/CacheService.js';
|
||||
import type { RoleCondFormulaValue } from '@/models/Role.js';
|
||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||
import type { GlobalEvents } from '@/core/GlobalEventService.js';
|
||||
|
@ -92,7 +93,7 @@ export const DEFAULT_POLICIES: RolePolicies = {
|
|||
canUpdateBioMedia: true,
|
||||
pinLimit: 5,
|
||||
antennaLimit: 5,
|
||||
wordMuteLimit: 200,
|
||||
wordMuteLimit: 1000,
|
||||
webhookLimit: 3,
|
||||
clipLimit: 10,
|
||||
noteEachClipsLimit: 200,
|
||||
|
@ -221,20 +222,20 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit {
|
|||
}
|
||||
|
||||
@bindThis
|
||||
private evalCond(user: MiUser, roles: MiRole[], value: RoleCondFormulaValue): boolean {
|
||||
private evalCond(user: MiUser, roles: MiRole[], value: RoleCondFormulaValue, followStats: FollowStats): boolean {
|
||||
try {
|
||||
switch (value.type) {
|
||||
// ~かつ~
|
||||
case 'and': {
|
||||
return value.values.every(v => this.evalCond(user, roles, v));
|
||||
return value.values.every(v => this.evalCond(user, roles, v, followStats));
|
||||
}
|
||||
// ~または~
|
||||
case 'or': {
|
||||
return value.values.some(v => this.evalCond(user, roles, v));
|
||||
return value.values.some(v => this.evalCond(user, roles, v, followStats));
|
||||
}
|
||||
// ~ではない
|
||||
case 'not': {
|
||||
return !this.evalCond(user, roles, value.value);
|
||||
return !this.evalCond(user, roles, value.value, followStats);
|
||||
}
|
||||
// マニュアルロールがアサインされている
|
||||
case 'roleAssignedTo': {
|
||||
|
@ -305,6 +306,30 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit {
|
|||
case 'followingMoreThanOrEq': {
|
||||
return user.followingCount >= value.value;
|
||||
}
|
||||
case 'localFollowersLessThanOrEq': {
|
||||
return followStats.localFollowers <= value.value;
|
||||
}
|
||||
case 'localFollowersMoreThanOrEq': {
|
||||
return followStats.localFollowers >= value.value;
|
||||
}
|
||||
case 'localFollowingLessThanOrEq': {
|
||||
return followStats.localFollowing <= value.value;
|
||||
}
|
||||
case 'localFollowingMoreThanOrEq': {
|
||||
return followStats.localFollowing >= value.value;
|
||||
}
|
||||
case 'remoteFollowersLessThanOrEq': {
|
||||
return followStats.remoteFollowers <= value.value;
|
||||
}
|
||||
case 'remoteFollowersMoreThanOrEq': {
|
||||
return followStats.remoteFollowers >= value.value;
|
||||
}
|
||||
case 'remoteFollowingLessThanOrEq': {
|
||||
return followStats.remoteFollowing <= value.value;
|
||||
}
|
||||
case 'remoteFollowingMoreThanOrEq': {
|
||||
return followStats.remoteFollowing >= value.value;
|
||||
}
|
||||
// ノート数が指定値以下
|
||||
case 'notesLessThanOrEq': {
|
||||
return user.notesCount <= value.value;
|
||||
|
@ -340,10 +365,11 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit {
|
|||
@bindThis
|
||||
public async getUserRoles(userId: MiUser['id']) {
|
||||
const roles = await this.rolesCache.fetch(() => this.rolesRepository.findBy({}));
|
||||
const followStats = await this.cacheService.getFollowStats(userId);
|
||||
const assigns = await this.getUserAssigns(userId);
|
||||
const assignedRoles = roles.filter(r => assigns.map(x => x.roleId).includes(r.id));
|
||||
const user = roles.some(r => r.target === 'conditional') ? await this.cacheService.findUserById(userId) : null;
|
||||
const matchedCondRoles = roles.filter(r => r.target === 'conditional' && this.evalCond(user!, assignedRoles, r.condFormula));
|
||||
const matchedCondRoles = roles.filter(r => r.target === 'conditional' && this.evalCond(user!, assignedRoles, r.condFormula, followStats));
|
||||
return [...assignedRoles, ...matchedCondRoles];
|
||||
}
|
||||
|
||||
|
@ -357,12 +383,13 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit {
|
|||
// 期限切れのロールを除外
|
||||
assigns = assigns.filter(a => a.expiresAt == null || (a.expiresAt.getTime() > now));
|
||||
const roles = await this.rolesCache.fetch(() => this.rolesRepository.findBy({}));
|
||||
const followStats = await this.cacheService.getFollowStats(userId);
|
||||
const assignedRoles = roles.filter(r => assigns.map(x => x.roleId).includes(r.id));
|
||||
const assignedBadgeRoles = assignedRoles.filter(r => r.asBadge);
|
||||
const badgeCondRoles = roles.filter(r => r.asBadge && (r.target === 'conditional'));
|
||||
if (badgeCondRoles.length > 0) {
|
||||
const user = roles.some(r => r.target === 'conditional') ? await this.cacheService.findUserById(userId) : null;
|
||||
const matchedBadgeCondRoles = badgeCondRoles.filter(r => this.evalCond(user!, assignedRoles, r.condFormula));
|
||||
const matchedBadgeCondRoles = badgeCondRoles.filter(r => this.evalCond(user!, assignedRoles, r.condFormula, followStats));
|
||||
return [...assignedBadgeRoles, ...matchedBadgeCondRoles];
|
||||
} else {
|
||||
return assignedBadgeRoles;
|
||||
|
|
|
@ -147,6 +147,70 @@ type CondFormulaValueFollowingMoreThanOrEq = {
|
|||
value: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* Is followed by at most N local users
|
||||
*/
|
||||
type CondFormulaValueLocalFollowersLessThanOrEq = {
|
||||
type: 'localFollowersLessThanOrEq';
|
||||
value: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* Is followed by at least N local users
|
||||
*/
|
||||
type CondFormulaValueLocalFollowersMoreThanOrEq = {
|
||||
type: 'localFollowersMoreThanOrEq';
|
||||
value: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* Is following at most N local users
|
||||
*/
|
||||
type CondFormulaValueLocalFollowingLessThanOrEq = {
|
||||
type: 'localFollowingLessThanOrEq';
|
||||
value: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* Is following at least N local users
|
||||
*/
|
||||
type CondFormulaValueLocalFollowingMoreThanOrEq = {
|
||||
type: 'localFollowingMoreThanOrEq';
|
||||
value: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* Is followed by at most N remote users
|
||||
*/
|
||||
type CondFormulaValueRemoteFollowersLessThanOrEq = {
|
||||
type: 'remoteFollowersLessThanOrEq';
|
||||
value: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* Is followed by at least N remote users
|
||||
*/
|
||||
type CondFormulaValueRemoteFollowersMoreThanOrEq = {
|
||||
type: 'remoteFollowersMoreThanOrEq';
|
||||
value: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* Is following at most N remote users
|
||||
*/
|
||||
type CondFormulaValueRemoteFollowingLessThanOrEq = {
|
||||
type: 'remoteFollowingLessThanOrEq';
|
||||
value: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* Is following at least N remote users
|
||||
*/
|
||||
type CondFormulaValueRemoteFollowingMoreThanOrEq = {
|
||||
type: 'remoteFollowingMoreThanOrEq';
|
||||
value: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* 投稿数が指定値以下の場合のみ成立とする
|
||||
*/
|
||||
|
@ -182,6 +246,14 @@ export type RoleCondFormulaValue = { id: string } & (
|
|||
CondFormulaValueFollowersMoreThanOrEq |
|
||||
CondFormulaValueFollowingLessThanOrEq |
|
||||
CondFormulaValueFollowingMoreThanOrEq |
|
||||
CondFormulaValueLocalFollowersLessThanOrEq |
|
||||
CondFormulaValueLocalFollowersMoreThanOrEq |
|
||||
CondFormulaValueLocalFollowingLessThanOrEq |
|
||||
CondFormulaValueLocalFollowingMoreThanOrEq |
|
||||
CondFormulaValueRemoteFollowersLessThanOrEq |
|
||||
CondFormulaValueRemoteFollowersMoreThanOrEq |
|
||||
CondFormulaValueRemoteFollowingLessThanOrEq |
|
||||
CondFormulaValueRemoteFollowingMoreThanOrEq |
|
||||
CondFormulaValueNotesLessThanOrEq |
|
||||
CondFormulaValueNotesMoreThanOrEq
|
||||
);
|
||||
|
|
|
@ -58,6 +58,12 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
if (ps.attachedToLocalUserOnly) query.andWhere('tag.attachedLocalUsersCount != 0');
|
||||
if (ps.attachedToRemoteUserOnly) query.andWhere('tag.attachedRemoteUsersCount != 0');
|
||||
|
||||
// Ignore hidden hashtags
|
||||
query.andWhere(`
|
||||
NOT EXISTS (
|
||||
SELECT 1 FROM meta WHERE tag.name = ANY(meta."hiddenTags")
|
||||
)`);
|
||||
|
||||
switch (ps.sort) {
|
||||
case '+mentionedUsers': query.orderBy('tag.mentionedUsersCount', 'DESC'); break;
|
||||
case '-mentionedUsers': query.orderBy('tag.mentionedUsersCount', 'ASC'); break;
|
||||
|
|
|
@ -330,8 +330,13 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
if (ps.chatScope !== undefined) updates.chatScope = ps.chatScope;
|
||||
|
||||
function checkMuteWordCount(mutedWords: (string[] | string)[], limit: number) {
|
||||
// TODO: ちゃんと数える
|
||||
const length = JSON.stringify(mutedWords).length;
|
||||
const length = mutedWords.reduce((sum, word) => {
|
||||
const wordLength = Array.isArray(word)
|
||||
? word.reduce((l, w) => l + w.length, 0)
|
||||
: word.length;
|
||||
return sum + wordLength;
|
||||
}, 0);
|
||||
|
||||
if (length > limit) {
|
||||
throw new ApiError(meta.errors.tooManyMutedWords);
|
||||
}
|
||||
|
|
|
@ -173,23 +173,17 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<div v-else-if="!hardMuted" :class="$style.muted" @click="muted = false">
|
||||
<I18n v-if="muted === 'sensitiveMute'" :src="i18n.ts.userSaysSomethingSensitive" tag="small">
|
||||
<template #name>
|
||||
<MkA v-user-preview="appearNote.userId" :to="userPage(appearNote.user)">
|
||||
<MkUserName :user="appearNote.user"/>
|
||||
</MkA>
|
||||
<MkUserName :user="appearNote.user"/>
|
||||
</template>
|
||||
</I18n>
|
||||
<I18n v-else-if="showSoftWordMutedWord !== true" :src="i18n.ts.userSaysSomething" tag="small">
|
||||
<template #name>
|
||||
<MkA v-user-preview="appearNote.userId" :to="userPage(appearNote.user)">
|
||||
<MkUserName :user="appearNote.user"/>
|
||||
</MkA>
|
||||
<MkUserName :user="appearNote.user"/>
|
||||
</template>
|
||||
</I18n>
|
||||
<I18n v-else :src="i18n.ts.userSaysSomethingAbout" tag="small">
|
||||
<template #name>
|
||||
<MkA v-user-preview="appearNote.userId" :to="userPage(appearNote.user)">
|
||||
<MkUserName :user="appearNote.user"/>
|
||||
</MkA>
|
||||
<MkUserName :user="appearNote.user"/>
|
||||
</template>
|
||||
<template #word>
|
||||
{{ Array.isArray(muted) ? muted.map(words => Array.isArray(words) ? words.join() : words).slice(0, 3).join(' ') : muted }}
|
||||
|
@ -1388,6 +1382,11 @@ function emitUpdReaction(emoji: string, delta: number) {
|
|||
padding: 8px;
|
||||
text-align: center;
|
||||
opacity: 0.7;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.muted:hover {
|
||||
background: var(--MI_THEME-buttonBg);
|
||||
}
|
||||
|
||||
.reactionOmitted {
|
||||
|
|
|
@ -232,9 +232,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<div v-else class="_panel" :class="$style.muted" @click="muted = false">
|
||||
<I18n :src="i18n.ts.userSaysSomething" tag="small">
|
||||
<template #name>
|
||||
<MkA v-user-preview="appearNote.userId" :to="userPage(appearNote.user)">
|
||||
<MkUserName :user="appearNote.user"/>
|
||||
</MkA>
|
||||
<MkUserName :user="appearNote.user"/>
|
||||
</template>
|
||||
</I18n>
|
||||
</div>
|
||||
|
@ -1199,5 +1197,10 @@ function animatedMFM() {
|
|||
padding: 8px;
|
||||
text-align: center;
|
||||
opacity: 0.7;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.muted:hover {
|
||||
background: var(--MI_THEME-buttonBg);
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -75,9 +75,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<div v-else :class="$style.muted" @click="muted = false">
|
||||
<I18n :src="i18n.ts.userSaysSomething" tag="small">
|
||||
<template #name>
|
||||
<MkA v-user-preview="note.userId" :to="userPage(note.user)">
|
||||
<MkUserName :user="note.user"/>
|
||||
</MkA>
|
||||
<MkUserName :user="appearNote.user"/>
|
||||
</template>
|
||||
</I18n>
|
||||
</div>
|
||||
|
@ -518,5 +516,10 @@ if (props.detail) {
|
|||
border: 1px solid var(--MI_THEME-divider);
|
||||
margin: 8px 8px 0 8px;
|
||||
border-radius: var(--MI-radius-sm);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.muted:hover {
|
||||
background: var(--MI_THEME-buttonBg);
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -25,7 +25,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<script setup lang="ts">
|
||||
import * as Misskey from 'misskey-js';
|
||||
import { computed, shallowRef } from 'vue';
|
||||
import type { FollowingFeedTab } from '@/utility/following-feed-utils.js';
|
||||
import type { FollowingFeedTab } from '@/types/following-feed.js';
|
||||
import type { Paging } from '@/components/MkPagination.vue';
|
||||
import { infoImageUrl } from '@/instance.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
|
|
|
@ -174,23 +174,17 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<div v-else-if="!hardMuted" :class="$style.muted" @click="muted = false">
|
||||
<I18n v-if="muted === 'sensitiveMute'" :src="i18n.ts.userSaysSomethingSensitive" tag="small">
|
||||
<template #name>
|
||||
<MkA v-user-preview="appearNote.userId" :to="userPage(appearNote.user)">
|
||||
<MkUserName :user="appearNote.user"/>
|
||||
</MkA>
|
||||
<MkUserName :user="appearNote.user"/>
|
||||
</template>
|
||||
</I18n>
|
||||
<I18n v-else-if="showSoftWordMutedWord !== true" :src="i18n.ts.userSaysSomething" tag="small">
|
||||
<template #name>
|
||||
<MkA v-user-preview="appearNote.userId" :to="userPage(appearNote.user)">
|
||||
<MkUserName :user="appearNote.user"/>
|
||||
</MkA>
|
||||
<MkUserName :user="appearNote.user"/>
|
||||
</template>
|
||||
</I18n>
|
||||
<I18n v-else :src="i18n.ts.userSaysSomethingAbout" tag="small">
|
||||
<template #name>
|
||||
<MkA v-user-preview="appearNote.userId" :to="userPage(appearNote.user)">
|
||||
<MkUserName :user="appearNote.user"/>
|
||||
</MkA>
|
||||
<MkUserName :user="appearNote.user"/>
|
||||
</template>
|
||||
<template #word>
|
||||
{{ Array.isArray(muted) ? muted.map(words => Array.isArray(words) ? words.join() : words).slice(0, 3).join(' ') : muted }}
|
||||
|
@ -1451,6 +1445,11 @@ function emitUpdReaction(emoji: string, delta: number) {
|
|||
padding: 8px;
|
||||
text-align: center;
|
||||
opacity: 0.7;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.muted:hover {
|
||||
background: var(--MI_THEME-buttonBg);
|
||||
}
|
||||
|
||||
.reactionOmitted {
|
||||
|
|
|
@ -237,9 +237,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<div v-else class="_panel" :class="$style.muted" @click="muted = false">
|
||||
<I18n :src="i18n.ts.userSaysSomething" tag="small">
|
||||
<template #name>
|
||||
<MkA v-user-preview="appearNote.userId" :to="userPage(appearNote.user)">
|
||||
<MkUserName :user="appearNote.user"/>
|
||||
</MkA>
|
||||
<MkUserName :user="appearNote.user"/>
|
||||
</template>
|
||||
</I18n>
|
||||
</div>
|
||||
|
@ -1273,6 +1271,11 @@ onUnmounted(() => {
|
|||
padding: 8px;
|
||||
text-align: center;
|
||||
opacity: 0.7;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.muted:hover {
|
||||
background: var(--MI_THEME-buttonBg);
|
||||
}
|
||||
|
||||
.badgeRoles {
|
||||
|
|
|
@ -83,9 +83,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<div v-else :class="$style.muted" @click="muted = false">
|
||||
<I18n :src="i18n.ts.userSaysSomething" tag="small">
|
||||
<template #name>
|
||||
<MkA v-user-preview="note.userId" :to="userPage(note.user)">
|
||||
<MkUserName :user="note.user"/>
|
||||
</MkA>
|
||||
<MkUserName :user="appearNote.user"/>
|
||||
</template>
|
||||
</I18n>
|
||||
</div>
|
||||
|
@ -606,6 +604,11 @@ if (props.detail) {
|
|||
border: 1px solid var(--MI_THEME-divider);
|
||||
margin: 8px 8px 0 8px;
|
||||
border-radius: var(--MI-radius-sm);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.muted:hover {
|
||||
background: var(--MI_THEME-buttonBg);
|
||||
}
|
||||
|
||||
// avatar container with line
|
||||
|
|
57
packages/frontend/src/components/SkPatternTest.vue
Normal file
57
packages/frontend/src/components/SkPatternTest.vue
Normal file
|
@ -0,0 +1,57 @@
|
|||
<!--
|
||||
SPDX-FileCopyrightText: hazelnoot and other Sharkey contributors
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
-->
|
||||
|
||||
<template>
|
||||
<MkFolder>
|
||||
<template #label>{{ i18n.ts.wordMuteTestLabel }}</template>
|
||||
|
||||
<div class="_gaps">
|
||||
<MkTextarea v-model="testWords">
|
||||
<template #caption>{{ i18n.ts.wordMuteTestDescription }}</template>
|
||||
</MkTextarea>
|
||||
<div><MkButton :disabled="!testWords" @click="testWordMutes">{{ i18n.ts.wordMuteTestTest }}</MkButton></div>
|
||||
<div v-if="testMatches == null">{{ i18n.ts.wordMuteTestNoResults}}</div>
|
||||
<div v-else-if="testMatches === ''">{{ i18n.ts.wordMuteTestNoMatch }}</div>
|
||||
<div v-else>{{ i18n.tsx.wordMuteTestMatch({ words: testMatches }) }}</div>
|
||||
</div>
|
||||
</MkFolder>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
import { i18n } from '@/i18n';
|
||||
import MkFolder from '@/components/MkFolder.vue';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import MkTextarea from '@/components/MkTextarea.vue';
|
||||
import { parseMutes } from '@/utility/parse-mutes';
|
||||
import { checkWordMute } from '@/utility/check-word-mute';
|
||||
|
||||
const props = defineProps<{
|
||||
mutedWords?: string | null,
|
||||
}>();
|
||||
|
||||
const testWords = ref<string | null>(null);
|
||||
const testMatches = ref<string | null>(null);
|
||||
|
||||
function testWordMutes() {
|
||||
if (!testWords.value || !props.mutedWords) {
|
||||
testMatches.value = null;
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const mutes = parseMutes(props.mutedWords);
|
||||
const matches = checkWordMute(testWords.value, null, mutes);
|
||||
testMatches.value = matches ? matches.flat(2).join(', ') : '';
|
||||
} catch {
|
||||
// Error is displayed by above function
|
||||
testMatches.value = null;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style module lang="scss">
|
||||
|
||||
</style>
|
|
@ -11,10 +11,10 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
import type { FollowingFeedModel } from '@/utility/following-feed-utils.js';
|
||||
import type { FollowingFeedModel } from '@/types/following-feed.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import MkInfo from '@/components/MkInfo.vue';
|
||||
import { followersTab } from '@/utility/following-feed-utils.js';
|
||||
import { followersTab } from '@/types/following-feed.js';
|
||||
|
||||
const props = defineProps<{
|
||||
model: FollowingFeedModel,
|
||||
|
|
|
@ -22,6 +22,14 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<option value="followersMoreThanOrEq">{{ i18n.ts._role._condition.followersMoreThanOrEq }}</option>
|
||||
<option value="followingLessThanOrEq">{{ i18n.ts._role._condition.followingLessThanOrEq }}</option>
|
||||
<option value="followingMoreThanOrEq">{{ i18n.ts._role._condition.followingMoreThanOrEq }}</option>
|
||||
<option value="localFollowersLessThanOrEq">{{ i18n.ts._role._condition.localFollowersLessThanOrEq }}</option>
|
||||
<option value="localFollowersMoreThanOrEq">{{ i18n.ts._role._condition.localFollowersMoreThanOrEq }}</option>
|
||||
<option value="localFollowingLessThanOrEq">{{ i18n.ts._role._condition.localFollowingLessThanOrEq }}</option>
|
||||
<option value="localFollowingMoreThanOrEq">{{ i18n.ts._role._condition.localFollowingMoreThanOrEq }}</option>
|
||||
<option value="remoteFollowersLessThanOrEq">{{ i18n.ts._role._condition.remoteFollowersLessThanOrEq }}</option>
|
||||
<option value="remoteFollowersMoreThanOrEq">{{ i18n.ts._role._condition.remoteFollowersMoreThanOrEq }}</option>
|
||||
<option value="remoteFollowingLessThanOrEq">{{ i18n.ts._role._condition.remoteFollowingLessThanOrEq }}</option>
|
||||
<option value="remoteFollowingMoreThanOrEq">{{ i18n.ts._role._condition.remoteFollowingMoreThanOrEq }}</option>
|
||||
<option value="notesLessThanOrEq">{{ i18n.ts._role._condition.notesLessThanOrEq }}</option>
|
||||
<option value="notesMoreThanOrEq">{{ i18n.ts._role._condition.notesMoreThanOrEq }}</option>
|
||||
<option value="and">{{ i18n.ts._role._condition.and }}</option>
|
||||
|
@ -56,7 +64,26 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<template #suffix>sec</template>
|
||||
</MkInput>
|
||||
|
||||
<MkInput v-else-if="['followersLessThanOrEq', 'followersMoreThanOrEq', 'followingLessThanOrEq', 'followingMoreThanOrEq', 'notesLessThanOrEq', 'notesMoreThanOrEq'].includes(type)" v-model="v.value" type="number">
|
||||
<MkInput
|
||||
v-else-if="[
|
||||
'followersLessThanOrEq',
|
||||
'followersMoreThanOrEq',
|
||||
'followingLessThanOrEq',
|
||||
'followingMoreThanOrEq',
|
||||
'localFollowersLessThanOrEq',
|
||||
'localFollowersMoreThanOrEq',
|
||||
'localFollowingLessThanOrEq',
|
||||
'localFollowingMoreThanOrEq',
|
||||
'remoteFollowersLessThanOrEq',
|
||||
'remoteFollowersMoreThanOrEq',
|
||||
'remoteFollowingLessThanOrEq',
|
||||
'remoteFollowingMoreThanOrEq',
|
||||
'notesLessThanOrEq',
|
||||
'notesMoreThanOrEq'
|
||||
].includes(type)"
|
||||
v-model="v.value"
|
||||
type="number"
|
||||
>
|
||||
</MkInput>
|
||||
|
||||
<MkSelect v-else-if="type === 'roleAssignedTo'" v-model="v.roleId">
|
||||
|
@ -70,6 +97,11 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<MkSwitch v-if="type === 'isFromInstance'" v-model="v.subdomains">
|
||||
<template #label>{{ i18n.ts._role._condition.isFromInstanceSubdomains }}</template>
|
||||
</MkSwitch>
|
||||
|
||||
<div v-if="['remoteFollowersLessThanOrEq', 'remoteFollowersMoreThanOrEq', 'remoteFollowingLessThanOrEq', 'remoteFollowingMoreThanOrEq'].includes(type)" :class="$style.warningBanner">
|
||||
<i class="ti ti-alert-triangle"></i>
|
||||
{{ i18n.ts._role.remoteDataWarning }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
@ -123,6 +155,14 @@ const type = computed({
|
|||
if (t === 'followersMoreThanOrEq') v.value.value = 10;
|
||||
if (t === 'followingLessThanOrEq') v.value.value = 10;
|
||||
if (t === 'followingMoreThanOrEq') v.value.value = 10;
|
||||
if (t === 'localFollowersLessThanOrEq') v.value.value = 10;
|
||||
if (t === 'localFollowersMoreThanOrEq') v.value.value = 10;
|
||||
if (t === 'localFollowingLessThanOrEq') v.value.value = 10;
|
||||
if (t === 'localFollowingMoreThanOrEq') v.value.value = 10;
|
||||
if (t === 'remoteFollowersLessThanOrEq') v.value.value = 10;
|
||||
if (t === 'remoteFollowersMoreThanOrEq') v.value.value = 10;
|
||||
if (t === 'remoteFollowingLessThanOrEq') v.value.value = 10;
|
||||
if (t === 'remoteFollowingMoreThanOrEq') v.value.value = 10;
|
||||
if (t === 'notesLessThanOrEq') v.value.value = 10;
|
||||
if (t === 'notesMoreThanOrEq') v.value.value = 10;
|
||||
if (t === 'isFromInstance') {
|
||||
|
@ -178,4 +218,14 @@ function removeSelf() {
|
|||
border-color: var(--MI_THEME-accent);
|
||||
}
|
||||
}
|
||||
|
||||
.warningBanner {
|
||||
color: var(--MI_THEME-warn);
|
||||
width: 100%;
|
||||
padding: 0 6px;
|
||||
|
||||
> i {
|
||||
margin-right: 4px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -47,6 +47,9 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<MkTextarea v-model="trustedLinkUrlPatterns">
|
||||
<template #caption>{{ i18n.ts.trustedLinkUrlPatternsDescription }}</template>
|
||||
</MkTextarea>
|
||||
|
||||
<SkPatternTest :mutedWords="trustedLinkUrlPatterns"></SkPatternTest>
|
||||
|
||||
<MkButton primary @click="save_trustedLinkUrlPatterns">{{ i18n.ts.save }}</MkButton>
|
||||
</div>
|
||||
</MkFolder>
|
||||
|
@ -71,6 +74,9 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<MkTextarea v-model="sensitiveWords">
|
||||
<template #caption>{{ i18n.ts.sensitiveWordsDescription }}<br>{{ i18n.ts.sensitiveWordsDescription2 }}</template>
|
||||
</MkTextarea>
|
||||
|
||||
<SkPatternTest :mutedWords="sensitiveWords"></SkPatternTest>
|
||||
|
||||
<MkButton primary @click="save_sensitiveWords">{{ i18n.ts.save }}</MkButton>
|
||||
</div>
|
||||
</MkFolder>
|
||||
|
@ -83,6 +89,9 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<MkTextarea v-model="prohibitedWords">
|
||||
<template #caption>{{ i18n.ts.prohibitedWordsDescription }}<br>{{ i18n.ts.prohibitedWordsDescription2 }}</template>
|
||||
</MkTextarea>
|
||||
|
||||
<SkPatternTest :mutedWords="prohibitedWords"></SkPatternTest>
|
||||
|
||||
<MkButton primary @click="save_prohibitedWords">{{ i18n.ts.save }}</MkButton>
|
||||
</div>
|
||||
</MkFolder>
|
||||
|
@ -95,6 +104,9 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<MkTextarea v-model="prohibitedWordsForNameOfUser">
|
||||
<template #caption>{{ i18n.ts.prohibitedWordsForNameOfUserDescription }}<br>{{ i18n.ts.prohibitedWordsDescription2 }}</template>
|
||||
</MkTextarea>
|
||||
|
||||
<SkPatternTest :mutedWords="prohibitedWordsForNameOfUser"></SkPatternTest>
|
||||
|
||||
<MkButton primary @click="save_prohibitedWordsForNameOfUser">{{ i18n.ts.save }}</MkButton>
|
||||
</div>
|
||||
</MkFolder>
|
||||
|
@ -166,6 +178,7 @@ import { definePage } from '@/page.js';
|
|||
import MkButton from '@/components/MkButton.vue';
|
||||
import FormLink from '@/components/form/link.vue';
|
||||
import MkFolder from '@/components/MkFolder.vue';
|
||||
import SkPatternTest from '@/components/SkPatternTest.vue';
|
||||
|
||||
const enableRegistration = ref<boolean>(false);
|
||||
const emailRequiredForSignup = ref<boolean>(false);
|
||||
|
|
|
@ -33,7 +33,8 @@ import { i18n } from '@/i18n.js';
|
|||
import MkSwiper from '@/components/MkSwiper.vue';
|
||||
import MkPageHeader from '@/components/global/MkPageHeader.vue';
|
||||
import SkUserRecentNotes from '@/components/SkUserRecentNotes.vue';
|
||||
import { createModel, createHeaderItem, followingFeedTabs, followingTabIcon, followingTabName, followingTab } from '@/utility/following-feed-utils.js';
|
||||
import { createModel, createHeaderItem, followingTabIcon, followingTabName } from '@/utility/following-feed-utils.js';
|
||||
import { followingTab, followingFeedTabs } from '@/types/following-feed.js';
|
||||
import SkFollowingRecentNotes from '@/components/SkFollowingRecentNotes.vue';
|
||||
import SkRemoteFollowersWarning from '@/components/SkRemoteFollowersWarning.vue';
|
||||
import { useRouter } from '@/router.js';
|
||||
|
|
|
@ -11,6 +11,9 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<template #caption>{{ i18n.ts._wordMute.muteWordsDescription }}<br>{{ i18n.ts._wordMute.muteWordsDescription2 }}</template>
|
||||
</MkTextarea>
|
||||
</div>
|
||||
|
||||
<SkPatternTest :mutedWords="mutedWords"></SkPatternTest>
|
||||
|
||||
<MkButton primary inline :disabled="!changed" @click="save()"><i class="ti ti-device-floppy"></i> {{ i18n.ts.save }}</MkButton>
|
||||
</div>
|
||||
</template>
|
||||
|
@ -19,8 +22,9 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
import { ref, watch } from 'vue';
|
||||
import MkTextarea from '@/components/MkTextarea.vue';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import * as os from '@/os.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { parseMutes } from '@/utility/parse-mutes';
|
||||
import SkPatternTest from '@/components/SkPatternTest.vue';
|
||||
|
||||
const props = defineProps<{
|
||||
muted: (string[] | string)[];
|
||||
|
@ -30,7 +34,7 @@ const emit = defineEmits<{
|
|||
(ev: 'save', value: (string[] | string)[]): void;
|
||||
}>();
|
||||
|
||||
const render = (mutedWords) => mutedWords.map(x => {
|
||||
const render = (mutedWords: (string | string[])[]) => mutedWords.map(x => {
|
||||
if (Array.isArray(x)) {
|
||||
return x.join(' ');
|
||||
} else {
|
||||
|
@ -46,47 +50,15 @@ watch(mutedWords, () => {
|
|||
});
|
||||
|
||||
async function save() {
|
||||
const parseMutes = (mutes) => {
|
||||
// split into lines, remove empty lines and unnecessary whitespace
|
||||
let lines = mutes.trim().split('\n').map(line => line.trim()).filter(line => line !== '');
|
||||
|
||||
// check each line if it is a RegExp or not
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const line = lines[i];
|
||||
const regexp = line.match(/^\/(.+)\/(.*)$/);
|
||||
if (regexp) {
|
||||
// check that the RegExp is valid
|
||||
try {
|
||||
new RegExp(regexp[1], regexp[2]);
|
||||
// note that regex lines will not be split by spaces!
|
||||
} catch (err: any) {
|
||||
// invalid syntax: do not save, do not reset changed flag
|
||||
os.alert({
|
||||
type: 'error',
|
||||
title: i18n.ts.regexpError,
|
||||
text: i18n.tsx.regexpErrorDescription({ tab: 'word mute', line: i + 1 }) + '\n' + err.toString(),
|
||||
});
|
||||
// re-throw error so these invalid settings are not saved
|
||||
throw err;
|
||||
}
|
||||
} else {
|
||||
lines[i] = line.split(' ');
|
||||
}
|
||||
}
|
||||
|
||||
return lines;
|
||||
};
|
||||
|
||||
let parsed;
|
||||
try {
|
||||
parsed = parseMutes(mutedWords.value);
|
||||
} catch (err) {
|
||||
const parsed = parseMutes(mutedWords.value);
|
||||
|
||||
emit('save', parsed);
|
||||
|
||||
changed.value = false;
|
||||
} catch {
|
||||
// already displayed error message in parseMutes
|
||||
return;
|
||||
}
|
||||
|
||||
emit('save', parsed);
|
||||
|
||||
changed.value = false;
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -11,10 +11,10 @@ import type { Plugin } from '@/plugin.js';
|
|||
import type { DeviceKind } from '@/utility/device-kind.js';
|
||||
import type { DeckProfile } from '@/deck.js';
|
||||
import type { PreferencesDefinition } from './manager.js';
|
||||
import type { FollowingFeedState } from '@/utility/following-feed-utils.js';
|
||||
import type { FollowingFeedState } from '@/types/following-feed.js';
|
||||
import { DEFAULT_DEVICE_KIND } from '@/utility/device-kind.js';
|
||||
import { searchEngineMap } from '@/utility/search-engine-map.js';
|
||||
import { defaultFollowingFeedState } from '@/utility/following-feed-utils.js';
|
||||
import { defaultFollowingFeedState } from '@/types/following-feed.js';
|
||||
|
||||
/** サウンド設定 */
|
||||
export type SoundStore = {
|
||||
|
|
|
@ -10,11 +10,11 @@ import darkTheme from '@@/themes/d-green-lime.json5';
|
|||
import { hemisphere } from '@@/js/intl-const.js';
|
||||
import type { DeviceKind } from '@/utility/device-kind.js';
|
||||
import type { Plugin } from '@/plugin.js';
|
||||
import type { FollowingFeedState } from '@/types/following-feed.js';
|
||||
import { miLocalStorage } from '@/local-storage.js';
|
||||
import { Pizzax } from '@/lib/pizzax.js';
|
||||
import { DEFAULT_DEVICE_KIND } from '@/utility/device-kind.js';
|
||||
import { defaultFollowingFeedState } from '@/utility/following-feed-utils.js';
|
||||
import type { FollowingFeedState } from '@/utility/following-feed-utils.js';
|
||||
import { defaultFollowingFeedState } from '@/types/following-feed.js';
|
||||
import { searchEngineMap } from '@/utility/search-engine-map.js';
|
||||
|
||||
/**
|
||||
|
|
36
packages/frontend/src/types/following-feed.ts
Normal file
36
packages/frontend/src/types/following-feed.ts
Normal file
|
@ -0,0 +1,36 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: hazelnoot and other Sharkey contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import type { WritableComputedRef } from 'vue';
|
||||
|
||||
export const followingTab = 'following' as const;
|
||||
export const mutualsTab = 'mutuals' as const;
|
||||
export const followersTab = 'followers' as const;
|
||||
export const followingFeedTabs = [followingTab, mutualsTab, followersTab] as const;
|
||||
export type FollowingFeedTab = typeof followingFeedTabs[number];
|
||||
|
||||
export type FollowingFeedState = {
|
||||
withNonPublic: boolean,
|
||||
withQuotes: boolean,
|
||||
withBots: boolean,
|
||||
withReplies: boolean,
|
||||
onlyFiles: boolean,
|
||||
userList: FollowingFeedTab,
|
||||
remoteWarningDismissed: boolean,
|
||||
};
|
||||
|
||||
export type FollowingFeedModel = {
|
||||
[Key in keyof FollowingFeedState]: WritableComputedRef<FollowingFeedState[Key]>;
|
||||
};
|
||||
|
||||
export const defaultFollowingFeedState: FollowingFeedState = {
|
||||
withNonPublic: false,
|
||||
withQuotes: false,
|
||||
withBots: true,
|
||||
withReplies: false,
|
||||
onlyFiles: false,
|
||||
userList: followingTab,
|
||||
remoteWarningDismissed: false,
|
||||
};
|
|
@ -19,18 +19,19 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<script lang="ts">
|
||||
import { computed, shallowRef } from 'vue';
|
||||
import type { Column } from '@/deck.js';
|
||||
import type { FollowingFeedState } from '@/utility/following-feed-utils.js';
|
||||
import type { FollowingFeedState } from '@/types/following-feed.js';
|
||||
export type FollowingColumn = Column & Partial<FollowingFeedState>;
|
||||
</script>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { FollowingFeedTab } from '@/utility/following-feed-utils.js';
|
||||
import type { FollowingFeedTab } from '@/types/following-feed.js';
|
||||
import type { MenuItem } from '@/types/menu.js';
|
||||
import { getColumn, updateColumn } from '@/deck.js';
|
||||
import XColumn from '@/ui/deck/column.vue';
|
||||
import SkFollowingRecentNotes from '@/components/SkFollowingRecentNotes.vue';
|
||||
import SkRemoteFollowersWarning from '@/components/SkRemoteFollowersWarning.vue';
|
||||
import { createModel, createOptionsMenu, followingTab, followingTabName, followingTabIcon, followingFeedTabs } from '@/utility/following-feed-utils.js';
|
||||
import { followingTab, followingFeedTabs } from '@/types/following-feed.js';
|
||||
import { createModel, createOptionsMenu, followingTabName, followingTabIcon } from '@/utility/following-feed-utils.js';
|
||||
import * as os from '@/os.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { useRouter } from '@/router.js';
|
||||
|
|
|
@ -4,12 +4,12 @@
|
|||
*/
|
||||
import * as Misskey from 'misskey-js';
|
||||
|
||||
export function checkWordMute(note: Misskey.entities.Note, me: Misskey.entities.UserLite | null | undefined, mutedWords: Array<string | string[]>): Array<string | string[]> | false {
|
||||
export function checkWordMute(note: string | Misskey.entities.Note, me: Misskey.entities.UserLite | null | undefined, mutedWords: Array<string | string[]>): Array<string | string[]> | false {
|
||||
// 自分自身
|
||||
if (me && (note.userId === me.id)) return false;
|
||||
if (me && typeof(note) === 'object' && (note.userId === me.id)) return false;
|
||||
|
||||
if (mutedWords.length > 0) {
|
||||
const text = getNoteText(note);
|
||||
const text = typeof(note) === 'object' ? getNoteText(note) : note;
|
||||
|
||||
if (text === '') return false;
|
||||
|
||||
|
|
|
@ -7,16 +7,12 @@ import { computed } from 'vue';
|
|||
import type { Ref, WritableComputedRef } from 'vue';
|
||||
import type { PageHeaderItem } from '@/types/page-header.js';
|
||||
import type { MenuItem } from '@/types/menu.js';
|
||||
import type { FollowingFeedTab, FollowingFeedState, FollowingFeedModel } from '@/types/following-feed.js';
|
||||
import { deepMerge } from '@/utility/merge.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { popupMenu } from '@/os.js';
|
||||
import { prefer } from '@/preferences.js';
|
||||
|
||||
export const followingTab = 'following' as const;
|
||||
export const mutualsTab = 'mutuals' as const;
|
||||
export const followersTab = 'followers' as const;
|
||||
export const followingFeedTabs = [followingTab, mutualsTab, followersTab] as const;
|
||||
export type FollowingFeedTab = typeof followingFeedTabs[number];
|
||||
import { followingTab, followersTab, mutualsTab, defaultFollowingFeedState } from '@/types/following-feed.js';
|
||||
|
||||
export function followingTabName(tab: FollowingFeedTab): string;
|
||||
export function followingTabName(tab: FollowingFeedTab | null | undefined): null;
|
||||
|
@ -33,30 +29,6 @@ export function followingTabIcon(tab: FollowingFeedTab | null | undefined): stri
|
|||
return 'ph-user-check ph-bold ph-lg';
|
||||
}
|
||||
|
||||
export type FollowingFeedModel = {
|
||||
[Key in keyof FollowingFeedState]: WritableComputedRef<FollowingFeedState[Key]>;
|
||||
};
|
||||
|
||||
export type FollowingFeedState = {
|
||||
withNonPublic: boolean,
|
||||
withQuotes: boolean,
|
||||
withBots: boolean,
|
||||
withReplies: boolean,
|
||||
onlyFiles: boolean,
|
||||
userList: FollowingFeedTab,
|
||||
remoteWarningDismissed: boolean,
|
||||
};
|
||||
|
||||
export const defaultFollowingFeedState: FollowingFeedState = {
|
||||
withNonPublic: false,
|
||||
withQuotes: false,
|
||||
withBots: true,
|
||||
withReplies: false,
|
||||
onlyFiles: false,
|
||||
userList: followingTab,
|
||||
remoteWarningDismissed: false,
|
||||
};
|
||||
|
||||
interface StorageInterface {
|
||||
readonly state: Ref<Partial<FollowingFeedState>>;
|
||||
save(updated: Partial<FollowingFeedState>): void;
|
||||
|
|
41
packages/frontend/src/utility/parse-mutes.ts
Normal file
41
packages/frontend/src/utility/parse-mutes.ts
Normal file
|
@ -0,0 +1,41 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import * as os from '@/os';
|
||||
import { i18n } from '@/i18n';
|
||||
|
||||
export type Mutes = (string | string[])[];
|
||||
|
||||
export function parseMutes(mutes: string): Mutes {
|
||||
// split into lines, remove empty lines and unnecessary whitespace
|
||||
const lines = mutes.trim().split('\n').map(line => line.trim()).filter(line => line !== '');
|
||||
const outLines: Mutes = Array.from(lines);
|
||||
|
||||
// check each line if it is a RegExp or not
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const line = lines[i];
|
||||
const regexp = line.match(/^\/(.+)\/(.*)$/);
|
||||
if (regexp) {
|
||||
// check that the RegExp is valid
|
||||
try {
|
||||
new RegExp(regexp[1], regexp[2]);
|
||||
// note that regex lines will not be split by spaces!
|
||||
} catch (err: any) {
|
||||
// invalid syntax: do not save, do not reset changed flag
|
||||
os.alert({
|
||||
type: 'error',
|
||||
title: i18n.ts.regexpError,
|
||||
text: i18n.tsx.regexpErrorDescription({ tab: 'word mute', line: i + 1 }) + '\n' + err.toString(),
|
||||
});
|
||||
// re-throw error so these invalid settings are not saved
|
||||
throw err;
|
||||
}
|
||||
} else {
|
||||
outLines[i] = line.split(' ');
|
||||
}
|
||||
}
|
||||
|
||||
return outLines;
|
||||
}
|
|
@ -245,6 +245,15 @@ _role:
|
|||
isFromInstance: "Is from a specific instance"
|
||||
isFromInstanceHost: "Hostname (case-insensitive)"
|
||||
isFromInstanceSubdomains: "Match subdomains"
|
||||
localFollowersLessThanOrEq: "Has X or fewer local followers"
|
||||
localFollowersMoreThanOrEq: "Has X or more local followers"
|
||||
localFollowingLessThanOrEq: "Follows X or fewer local accounts"
|
||||
localFollowingMoreThanOrEq: "Follows X or more local accounts"
|
||||
remoteFollowersLessThanOrEq: "Has X or fewer remote followers"
|
||||
remoteFollowersMoreThanOrEq: "Has X or more remote followers"
|
||||
remoteFollowingLessThanOrEq: "Follows X or fewer remote accounts"
|
||||
remoteFollowingMoreThanOrEq: "Follows X or more remote accounts"
|
||||
remoteDataWarning: "This condition may be incorrect for remote users."
|
||||
_emailUnavailable:
|
||||
banned: "This email address is banned"
|
||||
_signup:
|
||||
|
@ -543,3 +552,10 @@ enableProxyAccountDescription: "If disabled, then the proxy account will not be
|
|||
_confirmPollEdit:
|
||||
title: Are you sure you want to edit this poll
|
||||
text: Editing this poll will cause it to lose all previous votes
|
||||
|
||||
wordMuteTestLabel: "Test patterns"
|
||||
wordMuteTestDescription: "Enter some text here to test your word patterns. The matched words, if any, will be displayed below."
|
||||
wordMuteTestTest: "Test"
|
||||
wordMuteTestMatch: "Matched words: {words}"
|
||||
wordMuteTestNoResults: "No results yet, enter some text and click \"Test\" to check it."
|
||||
wordMuteTestNoMatch: "Text does not match any patterns."
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue