Merge remote-tracking branch 'upstream/develop' into nodeinfostats

This commit is contained in:
Kinetix 2025-01-26 13:20:26 -08:00
commit 326bc0a24f
27 changed files with 179 additions and 148 deletions

View file

@ -341,9 +341,9 @@ checkActivityPubGetSignature: false
# Upload or download file size limits (bytes)
#maxFileSize: 262144000
# timeout and maximum size for imports (e.g. note imports)
# timeout (in milliseconds) and maximum size for imports (e.g. note imports)
#import:
# downloadTimeout: 30
# downloadTimeout: 30000
# maxFileSize: 262144000
# PID File of master process

View file

@ -4,7 +4,7 @@ stages:
testCommit:
stage: test
image: node:iron
image: node:jod
services:
- postgres:15
- redis

16
locales/index.d.ts vendored
View file

@ -9020,6 +9020,10 @@ export interface Locale extends ILocale {
* Remove background
*/
"removeBackground": string;
/**
* ListenBrainz username
*/
"listenbrainz": string;
};
"_exportOrImport": {
/**
@ -10805,6 +10809,10 @@ export interface Locale extends ILocale {
* Date
*/
"date": string;
/**
* Boost (hold Shift for visibility menu)
*/
"renoteShift": string;
/**
* Quoted.
*/
@ -11525,6 +11533,14 @@ export interface Locale extends ILocale {
* Change the background color of text.
*/
"backgroundDescription": string;
/**
* Border
*/
"border": string;
/**
* Draw a border around the content.
*/
"borderDescription": string;
/**
* Plain
*/

View file

@ -4,7 +4,7 @@
"private": true,
"type": "module",
"engines": {
"node": "^20.10.0 || ^22.0.0"
"node": "^22.0.0"
},
"scripts": {
"start": "node ./built/boot/entry.js",

View file

@ -528,44 +528,44 @@ export class NoteEntityService implements OnModuleInit {
// パフォーマンスのためートが作成されてから2秒以上経っていない場合はリアクションを取得しない
const oldId = this.idService.gen(Date.now() - 2000);
const targetNotes: MiNote[] = [];
for (const note of notes) {
if (isPureRenote(note)) {
const reactionsCount = Object.values(this.reactionsBufferingService.mergeReactions(note.renote.reactions, bufferedReactions?.get(note.renote.id)?.deltas ?? {})).reduce((a, b) => a + b, 0);
if (reactionsCount === 0) {
myReactionsMap.set(note.renote.id, null);
} else if (reactionsCount <= note.renote.reactionAndUserPairCache.length + (bufferedReactions?.get(note.renote.id)?.pairs.length ?? 0)) {
const pairInBuffer = bufferedReactions?.get(note.renote.id)?.pairs.find(p => p[0] === meId);
if (pairInBuffer) {
myReactionsMap.set(note.renote.id, pairInBuffer[1]);
} else {
const pair = note.renote.reactionAndUserPairCache.find(p => p.startsWith(meId));
myReactionsMap.set(note.renote.id, pair ? pair.split('/')[1] : null);
}
} else {
idsNeedFetchMyReaction.add(note.renote.id);
// we may need to fetch 'my reaction' for renote target.
targetNotes.push(note.renote);
if (note.renote.reply) {
// idem if the renote is also a reply.
targetNotes.push(note.renote.reply);
}
} else if (note.reply) {
// idem for OP of a regular reply.
targetNotes.push(note.reply);
} else {
if (note.id < oldId) {
const reactionsCount = Object.values(this.reactionsBufferingService.mergeReactions(note.reactions, bufferedReactions?.get(note.id)?.deltas ?? {})).reduce((a, b) => a + b, 0);
if (reactionsCount === 0) {
myReactionsMap.set(note.id, null);
} else if (reactionsCount <= note.reactionAndUserPairCache.length + (bufferedReactions?.get(note.id)?.pairs.length ?? 0)) {
const pairInBuffer = bufferedReactions?.get(note.id)?.pairs.find(p => p[0] === meId);
if (pairInBuffer) {
myReactionsMap.set(note.id, pairInBuffer[1]);
} else {
const pair = note.reactionAndUserPairCache.find(p => p.startsWith(meId));
myReactionsMap.set(note.id, pair ? pair.split('/')[1] : null);
}
} else {
idsNeedFetchMyReaction.add(note.id);
}
targetNotes.push(note);
} else {
myReactionsMap.set(note.id, null);
}
}
}
for (const note of targetNotes) {
const reactionsCount = Object.values(this.reactionsBufferingService.mergeReactions(note.reactions, bufferedReactions?.get(note.id)?.deltas ?? {})).reduce((a, b) => a + b, 0);
if (reactionsCount === 0) {
myReactionsMap.set(note.id, null);
} else if (reactionsCount <= note.reactionAndUserPairCache.length + (bufferedReactions?.get(note.id)?.pairs.length ?? 0)) {
const pairInBuffer = bufferedReactions?.get(note.id)?.pairs.find(p => p[0] === meId);
if (pairInBuffer) {
myReactionsMap.set(note.id, pairInBuffer[1]);
} else {
const pair = note.reactionAndUserPairCache.find(p => p.startsWith(meId));
myReactionsMap.set(note.id, pair ? pair.split('/')[1] : null);
}
} else {
idsNeedFetchMyReaction.add(note.id);
}
}
const myReactions = idsNeedFetchMyReaction.size > 0 ? await this.noteReactionsRepository.findBy({
userId: meId,
noteId: In(Array.from(idsNeedFetchMyReaction)),

View file

@ -17,6 +17,7 @@ import { bindThis } from '@/decorators.js';
import { QueueLoggerService } from '../QueueLoggerService.js';
import type * as Bull from 'bullmq';
import type { DbUserImportJobData } from '../types.js';
import type { Config } from '@/config.js';
// TODO: 名前衝突時の動作を選べるようにする
@Injectable()
@ -24,6 +25,9 @@ export class ImportCustomEmojisProcessorService {
private logger: Logger;
constructor(
@Inject(DI.config)
private config: Config,
@Inject(DI.driveFilesRepository)
private driveFilesRepository: DriveFilesRepository,
@ -57,7 +61,7 @@ export class ImportCustomEmojisProcessorService {
try {
fs.writeFileSync(destPath, '', 'binary');
await this.downloadService.downloadUrl(file.url, destPath);
await this.downloadService.downloadUrl(file.url, destPath, { operationTimeout: this.config.import?.downloadTimeout, maxSize: this.config.import?.maxFileSize });
} catch (e) { // TODO: 何度か再試行
if (e instanceof Error || typeof e === 'string') {
this.logger.error(e);

View file

@ -626,7 +626,7 @@ export class ImportNotesProcessorService {
if (!exists) {
try {
await this.downloadService.downloadUrl(videos[0].url, filePath);
await this.downloadUrl(videos[0].url, filePath);
} catch (e) { // TODO: 何度か再試行
this.logger.error(e instanceof Error ? e : new Error(e as string));
}
@ -651,7 +651,7 @@ export class ImportNotesProcessorService {
if (!exists) {
try {
await this.downloadService.downloadUrl(file.media_url_https, filePath);
await this.downloadUrl(file.media_url_https, filePath);
} catch (e) { // TODO: 何度か再試行
this.logger.error(e instanceof Error ? e : new Error(e as string));
}

View file

@ -68,6 +68,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
for (let i = 0; i < ps.count; i++) {
ticketsPromises.push(this.registrationTicketsRepository.insertOne({
id: this.idService.gen(),
createdBy: me,
createdById: me.id,
expiresAt: ps.expiresAt ? new Date(ps.expiresAt) : null,
code: generateInviteCode(),
}));

View file

@ -3,7 +3,6 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { IsNull } from 'typeorm';
import { Inject, Injectable } from '@nestjs/common';
import type { EmojisRepository } from '@/models/_.js';
import { Endpoint } from '@/server/api/endpoint-base.js';
@ -59,7 +58,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
const emojis = await this.emojisRepository.createQueryBuilder()
.where('host IS NULL')
.orderBy('LOWER(category)', 'ASC')
.orderBy('LOWER(name)', 'ASC')
.addOrderBy('LOWER(name)', 'ASC')
.getMany();
return {
emojis: await this.emojiEntityService.packSimpleMany(emojis),

View file

@ -103,11 +103,42 @@ export default abstract class Channel {
public onMessage?(type: string, body: JsonValue): void;
public async assignMyReaction(note: Packed<'Note'>, noteEntityService: NoteEntityService) {
if (this.user && Object.keys(note.reactions).length > 0) {
const myReaction = await noteEntityService.populateMyReaction(note, this.user.id);
note.myReaction = myReaction;
public async assignMyReaction(note: Packed<'Note'>, noteEntityService: NoteEntityService): Promise<Packed<'Note'>> {
let changed = false;
// StreamingApiServerService creates a single EventEmitter per server process,
// so a new note arriving from redis gets de-serialised once per server process,
// and then that single object is passed to all active channels on each connection.
// If we didn't clone the notes here, different connections would asynchronously write
// different values to the same object, resulting in a random value being sent to each frontend. -- Dakkar
const clonedNote = { ...note };
if (this.user && isRenotePacked(note) && !isQuotePacked(note)) {
if (note.renote && Object.keys(note.renote.reactions).length > 0) {
const myReaction = await noteEntityService.populateMyReaction(note.renote, this.user.id);
if (myReaction) {
changed = true;
clonedNote.renote = { ...note.renote };
clonedNote.renote.myReaction = myReaction;
}
}
if (note.renote?.reply && Object.keys(note.renote.reply.reactions).length > 0) {
const myReaction = await noteEntityService.populateMyReaction(note.renote.reply, this.user.id);
if (myReaction) {
changed = true;
clonedNote.renote = { ...note.renote };
clonedNote.renote.reply = { ...note.renote.reply };
clonedNote.renote.reply.myReaction = myReaction;
}
}
}
if (this.user && note.reply && Object.keys(note.reply.reactions).length > 0) {
const myReaction = await noteEntityService.populateMyReaction(note.reply, this.user.id);
if (myReaction) {
changed = true;
clonedNote.reply = { ...note.reply };
clonedNote.reply.myReaction = myReaction;
}
}
return changed ? clonedNote : note;
}
}

View file

@ -65,24 +65,11 @@ class BubbleTimelineChannel extends Channel {
if (this.isNoteMutedOrBlocked(note)) return;
const reactionsToFetch = [];
if (this.user && isRenotePacked(note) && !isQuotePacked(note)) {
if (note.renote) {
reactionsToFetch.push(this.assignMyReaction(note.renote, this.noteEntityService));
if (note.renote.reply) {
reactionsToFetch.push(this.assignMyReaction(note.renote.reply, this.noteEntityService));
}
}
}
if (this.user && note.reply) {
reactionsToFetch.push(this.assignMyReaction(note.reply, this.noteEntityService));
}
const clonedNote = await this.assignMyReaction(note, this.noteEntityService);
await Promise.all(reactionsToFetch);
this.connection.cacheNote(clonedNote);
this.connection.cacheNote(note);
this.send('note', note);
this.send('note', clonedNote);
}
@bindThis

View file

@ -60,24 +60,11 @@ class GlobalTimelineChannel extends Channel {
if (this.isNoteMutedOrBlocked(note)) return;
const reactionsToFetch = [];
if (this.user && isRenotePacked(note) && !isQuotePacked(note)) {
if (note.renote) {
reactionsToFetch.push(this.assignMyReaction(note.renote, this.noteEntityService));
if (note.renote.reply) {
reactionsToFetch.push(this.assignMyReaction(note.renote.reply, this.noteEntityService));
}
}
}
if (this.user && note.reply) {
reactionsToFetch.push(this.assignMyReaction(note.reply, this.noteEntityService));
}
const clonedNote = await this.assignMyReaction(note, this.noteEntityService);
await Promise.all(reactionsToFetch);
this.connection.cacheNote(clonedNote);
this.connection.cacheNote(note);
this.send('note', note);
this.send('note', clonedNote);
}
@bindThis

View file

@ -81,24 +81,11 @@ class HomeTimelineChannel extends Channel {
if (this.isNoteMutedOrBlocked(note)) return;
const reactionsToFetch = [];
if (this.user && isRenotePacked(note) && !isQuotePacked(note)) {
if (note.renote) {
reactionsToFetch.push(this.assignMyReaction(note.renote, this.noteEntityService));
if (note.renote.reply) {
reactionsToFetch.push(this.assignMyReaction(note.renote.reply, this.noteEntityService));
}
}
}
if (this.user && note.reply) {
reactionsToFetch.push(this.assignMyReaction(note.reply, this.noteEntityService));
}
const clonedNote = await this.assignMyReaction(note, this.noteEntityService);
await Promise.all(reactionsToFetch);
this.connection.cacheNote(clonedNote);
this.connection.cacheNote(note);
this.send('note', note);
this.send('note', clonedNote);
}
@bindThis

View file

@ -98,24 +98,11 @@ class HybridTimelineChannel extends Channel {
}
}
const reactionsToFetch = [];
if (this.user && isRenotePacked(note) && !isQuotePacked(note)) {
if (note.renote) {
reactionsToFetch.push(this.assignMyReaction(note.renote, this.noteEntityService));
if (note.renote.reply) {
reactionsToFetch.push(this.assignMyReaction(note.renote.reply, this.noteEntityService));
}
}
}
if (this.user && note.reply) {
reactionsToFetch.push(this.assignMyReaction(note.reply, this.noteEntityService));
}
const clonedNote = await this.assignMyReaction(note, this.noteEntityService);
await Promise.all(reactionsToFetch);
this.connection.cacheNote(clonedNote);
this.connection.cacheNote(note);
this.send('note', note);
this.send('note', clonedNote);
}
@bindThis

View file

@ -70,24 +70,11 @@ class LocalTimelineChannel extends Channel {
if (this.isNoteMutedOrBlocked(note)) return;
const reactionsToFetch = [];
if (this.user && isRenotePacked(note) && !isQuotePacked(note)) {
if (note.renote) {
reactionsToFetch.push(this.assignMyReaction(note.renote, this.noteEntityService));
if (note.renote.reply) {
reactionsToFetch.push(this.assignMyReaction(note.renote.reply, this.noteEntityService));
}
}
}
if (this.user && note.reply) {
reactionsToFetch.push(this.assignMyReaction(note.reply, this.noteEntityService));
}
const clonedNote = await this.assignMyReaction(note, this.noteEntityService);
await Promise.all(reactionsToFetch);
this.connection.cacheNote(clonedNote);
this.connection.cacheNote(note);
this.send('note', note);
this.send('note', clonedNote);
}
@bindThis

View file

@ -186,11 +186,13 @@ function parseAndMergeCategories(input: string, root: CustomEmojiFolderTree): Cu
const parts = input.split('/').map(p => p.trim());
let currentNode: CustomEmojiFolderTree = root;
const currentPath = [];
for (const part of parts) {
currentPath.push(part);
let existingNode = currentNode.children.find((node) => node.value === part);
if (!existingNode) {
const newNode: CustomEmojiFolderTree = { value: part, category: input, children: [] };
const newNode: CustomEmojiFolderTree = { value: part, category: currentPath.join("/"), children: [] };
currentNode.children.push(newNode);
existingNode = newNode;
}

View file

@ -128,11 +128,12 @@ SPDX-License-Identifier: AGPL-3.0-only
<button
v-if="canRenote"
ref="renoteButton"
v-tooltip="renoteTooltip"
:class="$style.footerButton"
class="_button"
:style="renoted ? 'color: var(--MI_THEME-accent) !important;' : ''"
@click.stop
@mousedown.prevent="renoted ? undoRenote(appearNote) : boostVisibility()"
@mousedown.prevent="renoted ? undoRenote(appearNote) : boostVisibility($event.shiftKey)"
>
<i class="ti ti-repeat"></i>
<p v-if="appearNote.renoteCount > 0" :class="$style.footerButtonCount">{{ number(appearNote.renoteCount) }}</p>
@ -238,7 +239,7 @@ import { getNoteSummary } from '@/scripts/get-note-summary.js';
import MkRippleEffect from '@/components/MkRippleEffect.vue';
import { showMovedDialog } from '@/scripts/show-moved-dialog.js';
import { useRouter } from '@/router/supplier.js';
import { boostMenuItems, type Visibility } from '@/scripts/boost-quote.js';
import { boostMenuItems, type Visibility, computeRenoteTooltip } from '@/scripts/boost-quote.js';
import { isEnabledUrlPreview } from '@/instance.js';
import { type Keymap } from '@/scripts/hotkey.js';
import { focusPrev, focusNext } from '@/scripts/focus.js';
@ -338,6 +339,8 @@ const pleaseLoginContext = computed<OpenOnRemoteOptions>(() => ({
url: `https://${host}/notes/${appearNote.value.id}`,
}));
const renoteTooltip = computeRenoteTooltip(renoted);
/* Overload FunctionLint
function checkMute(noteToCheck: Misskey.entities.Note, mutedWords: Array<string | string[]> | undefined | null, checkOnly: true): boolean;
function checkMute(noteToCheck: Misskey.entities.Note, mutedWords: Array<string | string[]> | undefined | null, checkOnly: false): boolean | 'sensitiveMute';
@ -506,10 +509,10 @@ if (!props.mock) {
}
}
function boostVisibility() {
function boostVisibility(forceMenu: boolean = false) {
if (renoting) return;
if (!defaultStore.state.showVisibilitySelectorOnBoost) {
if (!defaultStore.state.showVisibilitySelectorOnBoost && !forceMenu) {
renote(defaultStore.state.visibilityOnBoost);
} else {
os.popupMenu(boostMenuItems(appearNote, renote), renoteButton.value);

View file

@ -140,10 +140,11 @@ SPDX-License-Identifier: AGPL-3.0-only
<button
v-if="canRenote"
ref="renoteButton"
v-tooltip="renoteTooltip"
class="_button"
:class="$style.noteFooterButton"
:style="renoted ? 'color: var(--MI_THEME-accent) !important;' : ''"
@mousedown.prevent="renoted ? undoRenote() : boostVisibility()"
@mousedown.prevent="renoted ? undoRenote() : boostVisibility($event.shiftKey)"
>
<i class="ti ti-repeat"></i>
<p v-if="appearNote.renoteCount > 0" :class="$style.noteFooterButtonCount">{{ number(appearNote.renoteCount) }}</p>
@ -280,7 +281,7 @@ import MkUserCardMini from '@/components/MkUserCardMini.vue';
import MkPagination, { type Paging } from '@/components/MkPagination.vue';
import MkReactionIcon from '@/components/MkReactionIcon.vue';
import MkButton from '@/components/MkButton.vue';
import { boostMenuItems, type Visibility } from '@/scripts/boost-quote.js';
import { boostMenuItems, type Visibility, computeRenoteTooltip } from '@/scripts/boost-quote.js';
import { isEnabledUrlPreview } from '@/instance.js';
import { getAppearNote } from '@/scripts/get-appear-note.js';
import { type Keymap } from '@/scripts/hotkey.js';
@ -347,6 +348,8 @@ const quotes = ref<Misskey.entities.Note[]>([]);
const canRenote = computed(() => ['public', 'home'].includes(appearNote.value.visibility) || (appearNote.value.visibility === 'followers' && appearNote.value.userId === $i?.id));
const defaultLike = computed(() => defaultStore.state.like ? defaultStore.state.like : null);
const renoteTooltip = computeRenoteTooltip(renoted);
watch(() => props.expandAllCws, (expandAllCws) => {
if (expandAllCws !== showContent.value) showContent.value = expandAllCws;
});
@ -478,10 +481,10 @@ useTooltip(quoteButton, async (showing) => {
});
});
function boostVisibility() {
function boostVisibility(forceMenu: boolean = false) {
if (renoting) return;
if (!defaultStore.state.showVisibilitySelectorOnBoost) {
if (!defaultStore.state.showVisibilitySelectorOnBoost && !forceMenu) {
renote(defaultStore.state.visibilityOnBoost);
} else {
os.popupMenu(boostMenuItems(appearNote, renote), renoteButton.value);

View file

@ -28,10 +28,11 @@ SPDX-License-Identifier: AGPL-3.0-only
<button
v-if="canRenote"
ref="renoteButton"
v-tooltip="renoteTooltip"
class="_button"
:class="$style.noteFooterButton"
:style="renoted ? 'color: var(--MI_THEME-accent) !important;' : ''"
@mousedown="renoted ? undoRenote() : boostVisibility()"
@mousedown="renoted ? undoRenote() : boostVisibility($event.shiftKey)"
>
<i class="ph-rocket-launch ph-bold ph-lg"></i>
<p v-if="note.renoteCount > 0" :class="$style.noteFooterButtonCount">{{ note.renoteCount }}</p>
@ -106,7 +107,7 @@ import { reactionPicker } from '@/scripts/reaction-picker.js';
import { claimAchievement } from '@/scripts/achievements.js';
import { getNoteMenu } from '@/scripts/get-note-menu.js';
import { useNoteCapture } from '@/scripts/use-note-capture.js';
import { boostMenuItems, type Visibility } from '@/scripts/boost-quote.js';
import { boostMenuItems, type Visibility, computeRenoteTooltip } from '@/scripts/boost-quote.js';
const canRenote = computed(() => ['public', 'home'].includes(props.note.visibility) || props.note.userId === $i.id);
@ -135,6 +136,8 @@ const quoteButton = shallowRef<HTMLElement>();
const menuButton = shallowRef<HTMLElement>();
const likeButton = shallowRef<HTMLElement>();
const renoteTooltip = computeRenoteTooltip(computed);
let appearNote = computed(() => isRenote ? props.note.renote as Misskey.entities.Note : props.note);
const defaultLike = computed(() => defaultStore.state.like ? defaultStore.state.like : null);
const replies = ref<Misskey.entities.Note[]>([]);
@ -285,8 +288,8 @@ watch(() => props.expandAllCws, (expandAllCws) => {
if (expandAllCws !== showContent.value) showContent.value = expandAllCws;
});
function boostVisibility() {
if (!defaultStore.state.showVisibilitySelectorOnBoost) {
function boostVisibility(forceMenu: boolean = false) {
if (!defaultStore.state.showVisibilitySelectorOnBoost && !forceMenu) {
renote(defaultStore.state.visibilityOnBoost);
} else {
os.popupMenu(boostMenuItems(appearNote, renote), renoteButton.value);

View file

@ -381,6 +381,16 @@ SPDX-License-Identifier: AGPL-3.0-only
</div>
</div>
</div>
<div class="section _block">
<div class="title">{{ i18n.ts._mfm.border }}</div>
<div class="content">
<p>{{ i18n.ts._mfm.borderDescription }}</p>
<div class="preview">
<Mfm :text="preview_border"/>
<MkTextarea v-model="preview_border"><span>MFM</span></MkTextarea>
</div>
</div>
</div>
<div class="section _block">
<div class="title">{{ i18n.ts._mfm.plain }}</div>
<div class="content">
@ -421,7 +431,7 @@ const preview_center = ref(
);
const preview_inlineCode = ref('`<: "Hello, world!"`');
const preview_blockCode = ref(
'```\n~ (#i, 100) {\n\t<: ? ((i % 15) = 0) "FizzBuzz"\n\t\t.? ((i % 3) = 0) "Fizz"\n\t\t.? ((i % 5) = 0) "Buzz"\n\t\t. i\n}\n```',
'```ai\n~ (#i, 100) {\n\t<: ? ((i % 15) = 0) "FizzBuzz"\n\t\t.? ((i % 3) = 0) "Fizz"\n\t\t.? ((i % 5) = 0) "Buzz"\n\t\t. i\n}\n```',
);
const preview_inlineMath = ref(
'\\(x= \\frac{-b\' \\pm \\sqrt{(b\')^2-ac}}{a}\\)',
@ -479,6 +489,7 @@ const preview_scale = ref(
);
const preview_fg = ref('$[fg.color=eb6f92 Text color]');
const preview_bg = ref('$[bg.color=31748f Background color]');
const preview_border = ref('$[border.color=eb6f92,style=outset,width=10,radius=10 Border]');
const preview_plain = ref(
'<plain>**bold** @mention #hashtag `code` $[x2 🍮]</plain>',
);

View file

@ -129,11 +129,12 @@ SPDX-License-Identifier: AGPL-3.0-only
<button
v-if="canRenote"
ref="renoteButton"
v-tooltip="renoteTooltip"
:class="$style.footerButton"
class="_button"
:style="renoted ? 'color: var(--MI_THEME-accent) !important;' : ''"
@click.stop
@mousedown.prevent="renoted ? undoRenote(appearNote) : boostVisibility()"
@mousedown.prevent="renoted ? undoRenote(appearNote) : boostVisibility($event.shiftKey)"
>
<i class="ti ti-repeat"></i>
<p v-if="appearNote.renoteCount > 0" :class="$style.footerButtonCount">{{ number(appearNote.renoteCount) }}</p>
@ -238,7 +239,7 @@ import { getNoteSummary } from '@/scripts/get-note-summary.js';
import MkRippleEffect from '@/components/MkRippleEffect.vue';
import { showMovedDialog } from '@/scripts/show-moved-dialog.js';
import { useRouter } from '@/router/supplier.js';
import { boostMenuItems, type Visibility } from '@/scripts/boost-quote.js';
import { boostMenuItems, type Visibility, computeRenoteTooltip } from '@/scripts/boost-quote.js';
import { isEnabledUrlPreview } from '@/instance.js';
import { type Keymap } from '@/scripts/hotkey.js';
import { focusPrev, focusNext } from '@/scripts/focus.js';
@ -333,6 +334,8 @@ const defaultLike = computed(() => defaultStore.state.like ? defaultStore.state.
const animated = computed(() => parsed.value ? checkAnimationFromMfm(parsed.value) : null);
const allowAnim = ref(defaultStore.state.advancedMfm && defaultStore.state.animatedMfm ? true : false);
const renoteTooltip = computeRenoteTooltip(renoted);
const pleaseLoginContext = computed<OpenOnRemoteOptions>(() => ({
type: 'lookup',
url: `https://${host}/notes/${appearNote.value.id}`,
@ -506,10 +509,10 @@ if (!props.mock) {
}
}
function boostVisibility() {
function boostVisibility(forceMenu: boolean = false) {
if (renoting) return;
if (!defaultStore.state.showVisibilitySelectorOnBoost) {
if (!defaultStore.state.showVisibilitySelectorOnBoost && !forceMenu) {
renote(defaultStore.state.visibilityOnBoost);
} else {
os.popupMenu(boostMenuItems(appearNote, renote), renoteButton.value);

View file

@ -145,10 +145,11 @@ SPDX-License-Identifier: AGPL-3.0-only
<button
v-if="canRenote"
ref="renoteButton"
v-tooltip="renoteTooltip"
class="_button"
:class="$style.noteFooterButton"
:style="renoted ? 'color: var(--MI_THEME-accent) !important;' : ''"
@mousedown.prevent="renoted ? undoRenote() : boostVisibility()"
@mousedown.prevent="renoted ? undoRenote() : boostVisibility($event.shiftKey)"
>
<i class="ti ti-repeat"></i>
<p v-if="appearNote.renoteCount > 0" :class="$style.noteFooterButtonCount">{{ number(appearNote.renoteCount) }}</p>
@ -285,7 +286,7 @@ import MkUserCardMini from '@/components/MkUserCardMini.vue';
import MkPagination, { type Paging } from '@/components/MkPagination.vue';
import MkReactionIcon from '@/components/MkReactionIcon.vue';
import MkButton from '@/components/MkButton.vue';
import { boostMenuItems, type Visibility } from '@/scripts/boost-quote.js';
import { boostMenuItems, type Visibility, computeRenoteTooltip } from '@/scripts/boost-quote.js';
import { isEnabledUrlPreview } from '@/instance.js';
import { getAppearNote } from '@/scripts/get-appear-note.js';
import { type Keymap } from '@/scripts/hotkey.js';
@ -353,6 +354,8 @@ const quotes = ref<Misskey.entities.Note[]>([]);
const canRenote = computed(() => ['public', 'home'].includes(appearNote.value.visibility) || (appearNote.value.visibility === 'followers' && appearNote.value.userId === $i?.id));
const defaultLike = computed(() => defaultStore.state.like ? defaultStore.state.like : null);
const renoteTooltip = computeRenoteTooltip(renoted);
watch(() => props.expandAllCws, (expandAllCws) => {
if (expandAllCws !== showContent.value) showContent.value = expandAllCws;
});
@ -484,10 +487,10 @@ useTooltip(quoteButton, async (showing) => {
});
});
function boostVisibility() {
function boostVisibility(forceMenu: boolean = false) {
if (renoting) return;
if (!defaultStore.state.showVisibilitySelectorOnBoost) {
if (!defaultStore.state.showVisibilitySelectorOnBoost && !forceMenu) {
renote(defaultStore.state.visibilityOnBoost);
} else {
os.popupMenu(boostMenuItems(appearNote, renote), renoteButton.value);

View file

@ -36,10 +36,11 @@ SPDX-License-Identifier: AGPL-3.0-only
<button
v-if="canRenote"
ref="renoteButton"
v-tooltip="renoteTooltip"
class="_button"
:class="$style.noteFooterButton"
:style="renoted ? 'color: var(--MI_THEME-accent) !important;' : ''"
@mousedown="renoted ? undoRenote() : boostVisibility()"
@mousedown="renoted ? undoRenote() : boostVisibility($event.shiftKey)"
>
<i class="ph-rocket-launch ph-bold ph-lg"></i>
<p v-if="note.renoteCount > 0" :class="$style.noteFooterButtonCount">{{ note.renoteCount }}</p>
@ -114,7 +115,7 @@ import { reactionPicker } from '@/scripts/reaction-picker.js';
import { claimAchievement } from '@/scripts/achievements.js';
import { getNoteMenu } from '@/scripts/get-note-menu.js';
import { useNoteCapture } from '@/scripts/use-note-capture.js';
import { boostMenuItems, type Visibility } from '@/scripts/boost-quote.js';
import { boostMenuItems, type Visibility, computeRenoteTooltip } from '@/scripts/boost-quote.js';
const canRenote = computed(() => ['public', 'home'].includes(props.note.visibility) || props.note.userId === $i.id);
const hideLine = computed(() => { return props.detail ? true : false; });
@ -149,6 +150,8 @@ const quoteButton = shallowRef<HTMLElement>();
const menuButton = shallowRef<HTMLElement>();
const likeButton = shallowRef<HTMLElement>();
const renoteTooltip = computeRenoteTooltip(renoted);
let appearNote = computed(() => isRenote ? props.note.renote as Misskey.entities.Note : props.note);
const defaultLike = computed(() => defaultStore.state.like ? defaultStore.state.like : null);
const replies = ref<Misskey.entities.Note[]>([]);
@ -299,8 +302,8 @@ watch(() => props.expandAllCws, (expandAllCws) => {
if (expandAllCws !== showContent.value) showContent.value = expandAllCws;
});
function boostVisibility() {
if (!defaultStore.state.showVisibilitySelectorOnBoost) {
function boostVisibility(forceMenu: boolean = false) {
if (!defaultStore.state.showVisibilitySelectorOnBoost && !forceMenu) {
renote(defaultStore.state.visibilityOnBoost);
} else {
os.popupMenu(boostMenuItems(appearNote, renote), renoteButton.value);

View file

@ -39,7 +39,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</MkInput>
<MkInput v-model="profile.listenbrainz" manualSave>
<template #label>ListenBrainz</template>
<template #label>{{ i18n.ts._profile.listenbrainz }}</template>
<template #prefix><i class="ph-headphones ph-bold ph-lg"></i></template>
</MkInput>

View file

@ -3,7 +3,7 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { ref, Ref } from 'vue';
import { ref, Ref, computed, ComputedRef } from 'vue';
import * as Misskey from 'misskey-js';
import { i18n } from '@/i18n.js';
import { defaultStore } from '@/store.js';
@ -79,3 +79,11 @@ export function boostMenuItems(appearNote: Ref<Misskey.entities.Note>, renote: (
} as MenuItem,
];
}
export function computeRenoteTooltip(renoted: Ref<boolean>): ComputedRef<string> {
return computed(() => {
if (renoted.value) return i18n.ts.unrenote;
if (defaultStore.state.showVisibilitySelectorOnBoost) return i18n.ts.renote;
return i18n.ts.renoteShift;
});
}

View file

@ -13,6 +13,7 @@ export default function sanitizeHtml(str: string | null): string | null {
...original.defaults.allowedAttributes,
a: original.defaults.allowedAttributes.a.concat(['style']),
img: original.defaults.allowedAttributes.img.concat(['style']),
'*': (original.defaults.allowedAttributes['*'] || []).concat(['style']),
},
});
}

View file

@ -17,6 +17,7 @@ emailDestination: "Destination address"
date: "Date"
renote: "Boost"
unrenote: "Remove boost"
renoteShift: "Boost (hold Shift for visibility menu)"
renoted: "Boosted."
quoted: "Quoted."
rmboost: "Unboosted."
@ -270,6 +271,7 @@ _profile:
changeBackground: "Change background"
updateBackground: "Update background"
removeBackground: "Remove background"
listenbrainz: "ListenBrainz username"
_timelines:
bubble: "Bubble"
_pages:
@ -383,6 +385,8 @@ _mfm:
fadeDescription: 'Fade text in and out.'
background: "Background color"
backgroundDescription: "Change the background color of text."
border: "Border"
borderDescription: "Draw a border around the content."
plain: "Plain"
plainDescription: "Deactivates the effects of all MFM contained within this MFM effect."
_animatedMFM: