release: 2023.11.2

This commit is contained in:
Marie 2023-12-01 00:01:19 +01:00 committed by GitHub
commit 1022280465
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
44 changed files with 706 additions and 407 deletions

View file

@ -112,6 +112,7 @@ redis:
# apiKey: '' # apiKey: ''
# ssl: true # ssl: true
# index: '' # index: ''
# scope: global
# ┌───────────────┐ # ┌───────────────┐
#───┘ ID generation └─────────────────────────────────────────── #───┘ ID generation └───────────────────────────────────────────
@ -144,15 +145,22 @@ id: 'aidx'
# Job concurrency per worker # Job concurrency per worker
# deliverJobConcurrency: 128 # deliverJobConcurrency: 128
# inboxJobConcurrency: 16 # inboxJobConcurrency: 16
# relashionshipJobConcurrency: 16
# What's relashionshipJob?:
# Follow, unfollow, block and unblock(ings) while following-imports, etc. or account migrations.
# Job rate limiter # Job rate limiter
# deliverJobPerSec: 128 # deliverJobPerSec: 128
# inboxJobPerSec: 16 # inboxJobPerSec: 16
# relashionshipJobPerSec: 64
# Job attempts # Job attempts
# deliverJobMaxAttempts: 12 # deliverJobMaxAttempts: 12
# inboxJobMaxAttempts: 8 # inboxJobMaxAttempts: 8
# Local address used for outgoing requests
#outgoingAddress: 127.0.0.1
# IP address family used for outgoing request (ipv4, ipv6 or dual) # IP address family used for outgoing request (ipv4, ipv6 or dual)
#outgoingAddressFamily: ipv4 #outgoingAddressFamily: ipv4
@ -175,8 +183,15 @@ proxyBypassHosts:
#mediaProxy: https://example.com/proxy #mediaProxy: https://example.com/proxy
# Proxy remote files (default: true) # Proxy remote files (default: true)
# Proxy remote files by this instance or mediaProxy to prevent remote files from running in remote domains.
proxyRemoteFiles: true proxyRemoteFiles: true
# Movie Thumbnail Generation URL
# There is no reference implementation.
# For example, Misskey will point to the following URL:
# https://example.com/thumbnail.webp?thumbnail=1&url=https%3A%2F%2Fstorage.example.com%2Fpath%2Fto%2Fvideo.mp4
#videoThumbnailGenerator: https://example.com
# Sign to ActivityPub GET request (default: true) # Sign to ActivityPub GET request (default: true)
signToActivityPubGet: true signToActivityPubGet: true

View file

@ -8,7 +8,7 @@ jobs:
run: run:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/first-interaction@v1.2.0 - uses: actions/first-interaction@v1.3.0
with: with:
repo-token: ${{ secrets.GITHUB_TOKEN }} repo-token: ${{ secrets.GITHUB_TOKEN }}
issue-message: | issue-message: |

View file

@ -55,6 +55,7 @@ addToAntenna: "Add to antenna"
sendMessage: "Send a message" sendMessage: "Send a message"
copyRSS: "Copy RSS" copyRSS: "Copy RSS"
copyUsername: "Copy username" copyUsername: "Copy username"
openRemoteProfile: "Open remote profile"
copyUserId: "Copy user ID" copyUserId: "Copy user ID"
copyNoteId: "Copy note ID" copyNoteId: "Copy note ID"
copyFileId: "Copy file ID" copyFileId: "Copy file ID"
@ -110,7 +111,6 @@ renote: "Boost"
unrenote: "Remove boost" unrenote: "Remove boost"
renoted: "Boosted." renoted: "Boosted."
quoted: "Quoted." quoted: "Quoted."
rmquote: "Removed quote."
rmboost: "Unboosted." rmboost: "Unboosted."
cantRenote: "This post can't be boosted." cantRenote: "This post can't be boosted."
cantReRenote: "A boost can't be boosted." cantReRenote: "A boost can't be boosted."
@ -891,6 +891,7 @@ continueThread: "View thread continuation"
deleteAccountConfirm: "This will irreversibly delete your account. Proceed?" deleteAccountConfirm: "This will irreversibly delete your account. Proceed?"
incorrectPassword: "Incorrect password." incorrectPassword: "Incorrect password."
voteConfirm: "Confirm your vote for \"{choice}\"?" voteConfirm: "Confirm your vote for \"{choice}\"?"
voteConfirmMulti: "Confirm your vote for \"{choice}\"?\n You can choose more options after confirmation."
hide: "Hide" hide: "Hide"
useDrawerReactionPickerForMobile: "Display reaction picker as drawer on mobile" useDrawerReactionPickerForMobile: "Display reaction picker as drawer on mobile"
welcomeBackWithName: "Welcome back, {name}" welcomeBackWithName: "Welcome back, {name}"
@ -987,6 +988,7 @@ cannotLoad: "Unable to load"
numberOfProfileView: "Profile views" numberOfProfileView: "Profile views"
like: "Like" like: "Like"
unlike: "Unlike" unlike: "Unlike"
defaultLike: "Default like emoji"
numberOfLikes: "Likes" numberOfLikes: "Likes"
show: "Show" show: "Show"
neverShow: "Don't show again" neverShow: "Don't show again"
@ -1855,6 +1857,14 @@ _ago:
monthsAgo: "{n}mo ago" monthsAgo: "{n}mo ago"
yearsAgo: "{n}y ago" yearsAgo: "{n}y ago"
invalid: "None" invalid: "None"
_timeIn:
seconds: "in {n} seconds"
minutes: "in {n} minutes"
hours: "in {n} hours"
days: "in {n} days"
weeks: "in {n} weeks"
months: "in {n} months"
years: "in {n} years"
_time: _time:
second: "Second(s)" second: "Second(s)"
minute: "Minute(s)" minute: "Minute(s)"
@ -1980,6 +1990,7 @@ _widgets:
_userList: _userList:
chooseList: "Select a list" chooseList: "Select a list"
clicker: "Clicker" clicker: "Clicker"
search: "Search"
_cw: _cw:
hide: "Hide" hide: "Hide"
show: "Show content" show: "Show content"
@ -2007,6 +2018,7 @@ _poll:
remainingHours: "{h} hour(s) {m} minute(s) remaining" remainingHours: "{h} hour(s) {m} minute(s) remaining"
remainingMinutes: "{m} minute(s) {s} second(s) remaining" remainingMinutes: "{m} minute(s) {s} second(s) remaining"
remainingSeconds: "{s} second(s) remaining" remainingSeconds: "{s} second(s) remaining"
multiple: "Multiple choices"
_visibility: _visibility:
public: "Public" public: "Public"
publicDescription: "Your note will be visible for all users" publicDescription: "Your note will be visible for all users"

6
locales/index.d.ts vendored
View file

@ -58,6 +58,7 @@ export interface Locale {
"sendMessage": string; "sendMessage": string;
"copyRSS": string; "copyRSS": string;
"copyUsername": string; "copyUsername": string;
"openRemoteProfile": string;
"copyUserId": string; "copyUserId": string;
"copyNoteId": string; "copyNoteId": string;
"copyFileId": string; "copyFileId": string;
@ -114,7 +115,6 @@ export interface Locale {
"renoted": string; "renoted": string;
"quoted": string; "quoted": string;
"rmboost": string; "rmboost": string;
"rmquote": string;
"cantRenote": string; "cantRenote": string;
"cantReRenote": string; "cantReRenote": string;
"quote": string; "quote": string;
@ -894,6 +894,7 @@ export interface Locale {
"deleteAccountConfirm": string; "deleteAccountConfirm": string;
"incorrectPassword": string; "incorrectPassword": string;
"voteConfirm": string; "voteConfirm": string;
"voteConfirmMulti": string;
"hide": string; "hide": string;
"useDrawerReactionPickerForMobile": string; "useDrawerReactionPickerForMobile": string;
"welcomeBackWithName": string; "welcomeBackWithName": string;
@ -990,6 +991,7 @@ export interface Locale {
"numberOfProfileView": string; "numberOfProfileView": string;
"like": string; "like": string;
"unlike": string; "unlike": string;
"defaultLike": string;
"numberOfLikes": string; "numberOfLikes": string;
"show": string; "show": string;
"neverShow": string; "neverShow": string;
@ -2125,6 +2127,7 @@ export interface Locale {
"chooseList": string; "chooseList": string;
}; };
"clicker": string; "clicker": string;
"search": string;
}; };
"_cw": { "_cw": {
"hide": string; "hide": string;
@ -2154,6 +2157,7 @@ export interface Locale {
"remainingHours": string; "remainingHours": string;
"remainingMinutes": string; "remainingMinutes": string;
"remainingSeconds": string; "remainingSeconds": string;
"multiple": string;
}; };
"_visibility": { "_visibility": {
"public": string; "public": string;

View file

@ -54,6 +54,7 @@ addToAntenna: "Aggiungi all'antenna"
sendMessage: "Invia messaggio" sendMessage: "Invia messaggio"
copyRSS: "Copia RSS" copyRSS: "Copia RSS"
copyUsername: "Copia nome utente" copyUsername: "Copia nome utente"
openRemoteProfile: "Apri profilo remoto"
copyUserId: "Copia ID del profilo" copyUserId: "Copia ID del profilo"
copyNoteId: "Copia ID della Nota" copyNoteId: "Copia ID della Nota"
copyFileId: "Copia ID del file" copyFileId: "Copia ID del file"
@ -731,6 +732,8 @@ thisIsExperimentalFeature: "Questa è una funzionalità sperimentale. Potrebbe e
developer: "Sviluppatore" developer: "Sviluppatore"
makeExplorable: "Profilo visibile pubblicamente nella pagina \"Esplora\"" makeExplorable: "Profilo visibile pubblicamente nella pagina \"Esplora\""
makeExplorableDescription: "Disabilitando questa opzione, il tuo profilo non verrà elencato nella pagina \"Esplora\"." makeExplorableDescription: "Disabilitando questa opzione, il tuo profilo non verrà elencato nella pagina \"Esplora\"."
makeIndexable: "Non indicizzare le note pubbliche"
makeIndexableDescription: "Le tue note pubbliche non saranno cercabili"
showGapBetweenNotesInTimeline: "Mostrare un intervallo tra le note sulla timeline" showGapBetweenNotesInTimeline: "Mostrare un intervallo tra le note sulla timeline"
duplicate: "Duplica" duplicate: "Duplica"
left: "Sinistra" left: "Sinistra"
@ -964,6 +967,7 @@ cannotLoad: "Caricamento impossibile"
numberOfProfileView: "Visualizzazioni profilo" numberOfProfileView: "Visualizzazioni profilo"
like: "Mi piace!" like: "Mi piace!"
unlike: "Non mi piace" unlike: "Non mi piace"
defaultLike: "Emoji predefinita per \"mi piace\""
numberOfLikes: "Numero di Like" numberOfLikes: "Numero di Like"
show: "Visualizza" show: "Visualizza"
neverShow: "Non mostrare più" neverShow: "Non mostrare più"
@ -1266,6 +1270,8 @@ _serverSettings:
shortName: "Abbreviazione" shortName: "Abbreviazione"
shortNameDescription: "Un'abbreviazione o un nome comune che può essere visualizzato al posto del nome ufficiale lungo del server." shortNameDescription: "Un'abbreviazione o un nome comune che può essere visualizzato al posto del nome ufficiale lungo del server."
fanoutTimelineDescription: "Attivando questa funzionalità migliori notevolmente la capacità delle Timeline di collezionare Note, riducendo il carico sul database. Tuttavia, aumenterà l'impiego di memoria RAM per Redis. Disattiva se il tuo server ha poca RAM o la funzionalità è irregolare." fanoutTimelineDescription: "Attivando questa funzionalità migliori notevolmente la capacità delle Timeline di collezionare Note, riducendo il carico sul database. Tuttavia, aumenterà l'impiego di memoria RAM per Redis. Disattiva se il tuo server ha poca RAM o la funzionalità è irregolare."
fanoutTimelineDbFallback: "Ripiega sul database"
fanoutTimelineDbFallbackDescription: "Attivando questa funzionalità, nel caso che il contenuto di una Timeline non sia presente nella cache, verrà consultato il database. Disattivandola, il carico sul database sarà ulteriormente ridotto, ma le Timeline saranno limitate"
_accountMigration: _accountMigration:
moveFrom: "Migra un altro profilo dentro a questo" moveFrom: "Migra un altro profilo dentro a questo"
moveFromSub: "Crea un alias verso un altro profilo remoto" moveFromSub: "Crea un alias verso un altro profilo remoto"
@ -1702,6 +1708,7 @@ _serverDisconnectedBehavior:
reload: "Ricarica automaticamente" reload: "Ricarica automaticamente"
dialog: "Apri avviso in finestra" dialog: "Apri avviso in finestra"
quiet: "Visualizza avviso in modo discreto" quiet: "Visualizza avviso in modo discreto"
disabled: "Non visualizzare l'avviso"
_channel: _channel:
create: "Nuovo canale" create: "Nuovo canale"
edit: "Gerisci canale" edit: "Gerisci canale"
@ -1817,6 +1824,14 @@ _ago:
monthsAgo: "{n} mesi fa" monthsAgo: "{n} mesi fa"
yearsAgo: "{n} anni fa" yearsAgo: "{n} anni fa"
invalid: "Niente da visualizzare" invalid: "Niente da visualizzare"
_timeIn:
seconds: "fra {n} secondi"
minutes: "fra {n} minuti"
hours: "fra {n} ore"
days: "fra {n} giorni"
weeks: "fra {n} settimane"
months: "fra {n} mesi"
years: "fra {n} anni"
_time: _time:
second: "s" second: "s"
minute: "min" minute: "min"

View file

@ -55,6 +55,7 @@ addToAntenna: "アンテナに追加"
sendMessage: "メッセージを送信" sendMessage: "メッセージを送信"
copyRSS: "RSSをコピー" copyRSS: "RSSをコピー"
copyUsername: "ユーザー名をコピー" copyUsername: "ユーザー名をコピー"
openRemoteProfile: "リモートプロファイルを開く"
copyUserId: "ユーザーIDをコピー" copyUserId: "ユーザーIDをコピー"
copyNoteId: "ートIDをコピー" copyNoteId: "ートIDをコピー"
copyFileId: "ファイルIDをコピー" copyFileId: "ファイルIDをコピー"
@ -111,7 +112,6 @@ unrenote: "リノート解除"
renoted: "ブースト。" renoted: "ブースト。"
quoted: "引用。" quoted: "引用。"
rmboost: "アンブースト。" rmboost: "アンブースト。"
rmquote: "引用を削除しました。"
cantRenote: "この投稿はリノートできません。" cantRenote: "この投稿はリノートできません。"
cantReRenote: "リノートをリノートすることはできません。" cantReRenote: "リノートをリノートすることはできません。"
quote: "引用" quote: "引用"
@ -891,6 +891,7 @@ continueThread: "さらにスレッドを見る"
deleteAccountConfirm: "アカウントが削除されます。よろしいですか?" deleteAccountConfirm: "アカウントが削除されます。よろしいですか?"
incorrectPassword: "パスワードが間違っています。" incorrectPassword: "パスワードが間違っています。"
voteConfirm: "「{choice}」に投票しますか?" voteConfirm: "「{choice}」に投票しますか?"
voteConfirmMulti: "「{choice}」に投票しますか?\n 確認後、選択肢を増やすことができます。"
hide: "隠す" hide: "隠す"
useDrawerReactionPickerForMobile: "モバイルデバイスのときドロワーで表示" useDrawerReactionPickerForMobile: "モバイルデバイスのときドロワーで表示"
welcomeBackWithName: "おかえりなさい、{name}さん" welcomeBackWithName: "おかえりなさい、{name}さん"
@ -987,6 +988,7 @@ cannotLoad: "読み込めません"
numberOfProfileView: "プロフィール表示回数" numberOfProfileView: "プロフィール表示回数"
like: "いいね!" like: "いいね!"
unlike: "いいねを解除" unlike: "いいねを解除"
defaultLike: "絵文字のようなデフォルト"
numberOfLikes: "いいね数" numberOfLikes: "いいね数"
show: "表示" show: "表示"
neverShow: "今後表示しない" neverShow: "今後表示しない"
@ -2029,6 +2031,7 @@ _widgets:
_userList: _userList:
chooseList: "リストを選択" chooseList: "リストを選択"
clicker: "クリッカー" clicker: "クリッカー"
search: "検索"
_cw: _cw:
hide: "隠す" hide: "隠す"
@ -2058,6 +2061,7 @@ _poll:
remainingHours: "終了まであと{h}時間{m}分" remainingHours: "終了まであと{h}時間{m}分"
remainingMinutes: "終了まであと{m}分{s}秒" remainingMinutes: "終了まであと{m}分{s}秒"
remainingSeconds: "終了まであと{s}秒" remainingSeconds: "終了まであと{s}秒"
multiple: "複数の選択肢"
_visibility: _visibility:
public: "パブリック" public: "パブリック"

View file

@ -1,6 +1,6 @@
{ {
"name": "sharkey", "name": "sharkey",
"version": "2023.11.1", "version": "2023.11.2",
"codename": "shonk", "codename": "shonk",
"repository": { "repository": {
"type": "git", "type": "git",

View file

@ -12,7 +12,7 @@ import { EmojiEntityService } from '@/core/entities/EmojiEntityService.js';
import { GlobalEventService } from '@/core/GlobalEventService.js'; import { GlobalEventService } from '@/core/GlobalEventService.js';
import type { MiDriveFile } from '@/models/DriveFile.js'; import type { MiDriveFile } from '@/models/DriveFile.js';
import type { MiEmoji } from '@/models/Emoji.js'; import type { MiEmoji } from '@/models/Emoji.js';
import type { EmojisRepository, MiRole, MiUser } from '@/models/_.js'; import type { DriveFilesRepository, EmojisRepository, MiRole, MiUser } from '@/models/_.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { MemoryKVCache, RedisSingleCache } from '@/misc/cache.js'; import { MemoryKVCache, RedisSingleCache } from '@/misc/cache.js';
import { UtilityService } from '@/core/UtilityService.js'; import { UtilityService } from '@/core/UtilityService.js';
@ -20,6 +20,7 @@ import { query } from '@/misc/prelude/url.js';
import type { Serialized } from '@/types.js'; import type { Serialized } from '@/types.js';
import { ModerationLogService } from '@/core/ModerationLogService.js'; import { ModerationLogService } from '@/core/ModerationLogService.js';
import type { Config } from '@/config.js'; import type { Config } from '@/config.js';
import { DriveService } from './DriveService.js';
const parseEmojiStrRegexp = /^(\w+)(?:@([\w.-]+))?$/; const parseEmojiStrRegexp = /^(\w+)(?:@([\w.-]+))?$/;
@ -38,11 +39,15 @@ export class CustomEmojiService implements OnApplicationShutdown {
@Inject(DI.emojisRepository) @Inject(DI.emojisRepository)
private emojisRepository: EmojisRepository, private emojisRepository: EmojisRepository,
@Inject(DI.driveFilesRepository)
private driveFilesRepository: DriveFilesRepository,
private utilityService: UtilityService, private utilityService: UtilityService,
private idService: IdService, private idService: IdService,
private emojiEntityService: EmojiEntityService, private emojiEntityService: EmojiEntityService,
private moderationLogService: ModerationLogService, private moderationLogService: ModerationLogService,
private globalEventService: GlobalEventService, private globalEventService: GlobalEventService,
private driveService: DriveService,
) { ) {
this.cache = new MemoryKVCache<MiEmoji | null>(1000 * 60 * 60 * 12); this.cache = new MemoryKVCache<MiEmoji | null>(1000 * 60 * 60 * 12);
@ -259,6 +264,12 @@ export class CustomEmojiService implements OnApplicationShutdown {
this.localEmojisCache.refresh(); this.localEmojisCache.refresh();
const file = await this.driveFilesRepository.findOneBy({ url: emoji.originalUrl, userHost: emoji.host ? emoji.host : IsNull() });
if (file) {
await this.driveService.deleteFile(file, false, moderator ? moderator : undefined);
}
this.globalEventService.publishBroadcastStream('emojiDeleted', { this.globalEventService.publishBroadcastStream('emojiDeleted', {
emojis: [await this.emojiEntityService.packDetailed(emoji)], emojis: [await this.emojiEntityService.packDetailed(emoji)],
}); });
@ -280,6 +291,12 @@ export class CustomEmojiService implements OnApplicationShutdown {
for (const emoji of emojis) { for (const emoji of emojis) {
await this.emojisRepository.delete(emoji.id); await this.emojisRepository.delete(emoji.id);
const file = await this.driveFilesRepository.findOneBy({ url: emoji.originalUrl, userHost: emoji.host ? emoji.host : IsNull() });
if (file) {
await this.driveService.deleteFile(file, false, moderator ? moderator : undefined);
}
if (moderator) { if (moderator) {
this.moderationLogService.log(moderator, 'deleteCustomEmoji', { this.moderationLogService.log(moderator, 'deleteCustomEmoji', {
emojiId: emoji.id, emojiId: emoji.id,

View file

@ -14,18 +14,15 @@ import { extractCustomEmojisFromMfm } from '@/misc/extract-custom-emojis-from-mf
import { extractHashtags } from '@/misc/extract-hashtags.js'; import { extractHashtags } from '@/misc/extract-hashtags.js';
import type { IMentionedRemoteUsers } from '@/models/Note.js'; import type { IMentionedRemoteUsers } from '@/models/Note.js';
import { MiNote } from '@/models/Note.js'; import { MiNote } from '@/models/Note.js';
import type { NoteEditRepository, ChannelFollowingsRepository, ChannelsRepository, FollowingsRepository, InstancesRepository, MiFollowing, MutingsRepository, NotesRepository, NoteThreadMutingsRepository, UserListMembershipsRepository, UserProfilesRepository, UsersRepository } from '@/models/_.js'; import type { NoteEditRepository, ChannelFollowingsRepository, ChannelsRepository, FollowingsRepository, InstancesRepository, MiFollowing, MutingsRepository, NotesRepository, NoteThreadMutingsRepository, UserListMembershipsRepository, UserProfilesRepository, UsersRepository, PollsRepository } from '@/models/_.js';
import type { MiDriveFile } from '@/models/DriveFile.js'; import type { MiDriveFile } from '@/models/DriveFile.js';
import type { MiApp } from '@/models/App.js'; import type { MiApp } from '@/models/App.js';
import { concat } from '@/misc/prelude/array.js'; import { concat } from '@/misc/prelude/array.js';
import { IdService } from '@/core/IdService.js'; import { IdService } from '@/core/IdService.js';
import type { MiUser, MiLocalUser, MiRemoteUser } from '@/models/User.js'; import type { MiUser, MiLocalUser, MiRemoteUser } from '@/models/User.js';
import { MiPoll, type IPoll } from '@/models/Poll.js'; import { MiPoll, type IPoll } from '@/models/Poll.js';
import { checkWordMute } from '@/misc/check-word-mute.js';
import type { MiChannel } from '@/models/Channel.js'; import type { MiChannel } from '@/models/Channel.js';
import { normalizeForSearch } from '@/misc/normalize-for-search.js'; import { normalizeForSearch } from '@/misc/normalize-for-search.js';
import { MemorySingleCache } from '@/misc/cache.js';
import type { MiUserProfile } from '@/models/UserProfile.js';
import { RelayService } from '@/core/RelayService.js'; import { RelayService } from '@/core/RelayService.js';
import { FederatedInstanceService } from '@/core/FederatedInstanceService.js'; import { FederatedInstanceService } from '@/core/FederatedInstanceService.js';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
@ -35,7 +32,6 @@ import ActiveUsersChart from '@/core/chart/charts/active-users.js';
import { GlobalEventService } from '@/core/GlobalEventService.js'; import { GlobalEventService } from '@/core/GlobalEventService.js';
import { NotificationService } from '@/core/NotificationService.js'; import { NotificationService } from '@/core/NotificationService.js';
import { WebhookService } from '@/core/WebhookService.js'; import { WebhookService } from '@/core/WebhookService.js';
import { HashtagService } from '@/core/HashtagService.js';
import { QueueService } from '@/core/QueueService.js'; import { QueueService } from '@/core/QueueService.js';
import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js';
@ -48,11 +44,7 @@ import { DB_MAX_NOTE_TEXT_LENGTH } from '@/const.js';
import { RoleService } from '@/core/RoleService.js'; import { RoleService } from '@/core/RoleService.js';
import { MetaService } from '@/core/MetaService.js'; import { MetaService } from '@/core/MetaService.js';
import { SearchService } from '@/core/SearchService.js'; import { SearchService } from '@/core/SearchService.js';
import { FeaturedService } from '@/core/FeaturedService.js';
import { FunoutTimelineService } from '@/core/FunoutTimelineService.js'; import { FunoutTimelineService } from '@/core/FunoutTimelineService.js';
import { AntennaService } from './AntennaService.js';
import NotesChart from './chart/charts/notes.js';
import PerUserNotesChart from './chart/charts/per-user-notes.js';
import { UtilityService } from '@/core/UtilityService.js'; import { UtilityService } from '@/core/UtilityService.js';
type NotificationType = 'reply' | 'renote' | 'quote' | 'mention'; type NotificationType = 'reply' | 'renote' | 'quote' | 'mention';
@ -191,6 +183,9 @@ export class NoteEditService implements OnApplicationShutdown {
@Inject(DI.noteEditRepository) @Inject(DI.noteEditRepository)
private noteEditRepository: NoteEditRepository, private noteEditRepository: NoteEditRepository,
@Inject(DI.pollsRepository)
private pollsRepository: PollsRepository,
private userEntityService: UserEntityService, private userEntityService: UserEntityService,
private noteEntityService: NoteEntityService, private noteEntityService: NoteEntityService,
private idService: IdService, private idService: IdService,
@ -201,18 +196,13 @@ export class NoteEditService implements OnApplicationShutdown {
private notificationService: NotificationService, private notificationService: NotificationService,
private relayService: RelayService, private relayService: RelayService,
private federatedInstanceService: FederatedInstanceService, private federatedInstanceService: FederatedInstanceService,
private hashtagService: HashtagService,
private antennaService: AntennaService,
private webhookService: WebhookService, private webhookService: WebhookService,
private featuredService: FeaturedService,
private remoteUserResolveService: RemoteUserResolveService, private remoteUserResolveService: RemoteUserResolveService,
private apDeliverManagerService: ApDeliverManagerService, private apDeliverManagerService: ApDeliverManagerService,
private apRendererService: ApRendererService, private apRendererService: ApRendererService,
private roleService: RoleService, private roleService: RoleService,
private metaService: MetaService, private metaService: MetaService,
private searchService: SearchService, private searchService: SearchService,
private notesChart: NotesChart,
private perUserNotesChart: PerUserNotesChart,
private activeUsersChart: ActiveUsersChart, private activeUsersChart: ActiveUsersChart,
private instanceChart: InstanceChart, private instanceChart: InstanceChart,
private utilityService: UtilityService, private utilityService: UtilityService,
@ -385,6 +375,10 @@ export class NoteEditService implements OnApplicationShutdown {
update.hasPoll = !!data.poll; update.hasPoll = !!data.poll;
} }
const poll = await this.pollsRepository.findOneBy({ noteId: oldnote.id });
const oldPoll = poll ? { choices: poll.choices, multiple: poll.multiple, expiresAt: poll.expiresAt } : null;
if (Object.keys(update).length > 0) { if (Object.keys(update).length > 0) {
const exists = await this.noteEditRepository.findOneBy({ noteId: oldnote.id }); const exists = await this.noteEditRepository.findOneBy({ noteId: oldnote.id });
@ -456,7 +450,7 @@ export class NoteEditService implements OnApplicationShutdown {
})); }));
} }
if (data.poll != null) { if (data.poll != null && JSON.stringify(data.poll) !== JSON.stringify(oldPoll)) {
// Start transaction // Start transaction
await this.db.transaction(async transactionalEntityManager => { await this.db.transaction(async transactionalEntityManager => {
await transactionalEntityManager.update(MiNote, oldnote.id, note); await transactionalEntityManager.update(MiNote, oldnote.id, note);

View file

@ -278,14 +278,14 @@ export class QueueService {
} }
@bindThis @bindThis
public createImportMastoToDbJob(user: ThinUser, targets: string[]) { public createImportMastoToDbJob(user: ThinUser, targets: string[], note: MiNote['id'] | null) {
const jobs = targets.map(rel => this.generateToDbJobData('importMastoToDb', { user, target: rel })); const jobs = targets.map(rel => this.generateToDbJobData('importMastoToDb', { user, target: rel, note }));
return this.dbQueue.addBulk(jobs); return this.dbQueue.addBulk(jobs);
} }
@bindThis @bindThis
public createImportPleroToDbJob(user: ThinUser, targets: string[]) { public createImportPleroToDbJob(user: ThinUser, targets: string[], note: MiNote['id'] | null) {
const jobs = targets.map(rel => this.generateToDbJobData('importPleroToDb', { user, target: rel })); const jobs = targets.map(rel => this.generateToDbJobData('importPleroToDb', { user, target: rel, note }));
return this.dbQueue.addBulk(jobs); return this.dbQueue.addBulk(jobs);
} }

View file

@ -171,6 +171,7 @@ export class ApRendererService {
mediaType: file.webpublicType ?? file.type, mediaType: file.webpublicType ?? file.type,
url: this.driveFileEntityService.getPublicUrl(file), url: this.driveFileEntityService.getPublicUrl(file),
name: file.comment, name: file.comment,
summary: file.comment,
}; };
} }

View file

@ -11,7 +11,7 @@ export interface IObject {
type: string | string[]; type: string | string[];
id?: string; id?: string;
name?: string | null; name?: string | null;
summary?: string; summary?: string | null;
_misskey_summary?: string; _misskey_summary?: string;
published?: string; published?: string;
cc?: ApObject; cc?: ApObject;

View file

@ -3,7 +3,7 @@ import * as vm from 'node:vm';
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import { ZipReader } from 'slacc'; import { ZipReader } from 'slacc';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import type { UsersRepository, DriveFilesRepository, MiDriveFile, MiNote, NotesRepository, MiUser } from '@/models/_.js'; import type { UsersRepository, DriveFilesRepository, MiDriveFile, MiNote, NotesRepository, MiUser, DriveFoldersRepository, MiDriveFolder } from '@/models/_.js';
import type Logger from '@/logger.js'; import type Logger from '@/logger.js';
import { DownloadService } from '@/core/DownloadService.js'; import { DownloadService } from '@/core/DownloadService.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
@ -14,9 +14,10 @@ import { DriveService } from '@/core/DriveService.js';
import { MfmService } from '@/core/MfmService.js'; import { MfmService } from '@/core/MfmService.js';
import { ApNoteService } from '@/core/activitypub/models/ApNoteService.js'; import { ApNoteService } from '@/core/activitypub/models/ApNoteService.js';
import { extractApHashtagObjects } from '@/core/activitypub/models/tag.js'; import { extractApHashtagObjects } from '@/core/activitypub/models/tag.js';
import { IdService } from '@/core/IdService.js';
import { QueueLoggerService } from '../QueueLoggerService.js'; import { QueueLoggerService } from '../QueueLoggerService.js';
import type * as Bull from 'bullmq'; import type * as Bull from 'bullmq';
import type { DbNoteImportToDbJobData, DbNoteImportJobData, DbKeyNoteImportToDbJobData } from '../types.js'; import type { DbNoteImportToDbJobData, DbNoteImportJobData, DbNoteWithParentImportToDbJobData } from '../types.js';
@Injectable() @Injectable()
export class ImportNotesProcessorService { export class ImportNotesProcessorService {
@ -29,6 +30,9 @@ export class ImportNotesProcessorService {
@Inject(DI.driveFilesRepository) @Inject(DI.driveFilesRepository)
private driveFilesRepository: DriveFilesRepository, private driveFilesRepository: DriveFilesRepository,
@Inject(DI.driveFoldersRepository)
private driveFoldersRepository: DriveFoldersRepository,
@Inject(DI.notesRepository) @Inject(DI.notesRepository)
private notesRepository: NotesRepository, private notesRepository: NotesRepository,
@ -38,20 +42,21 @@ export class ImportNotesProcessorService {
private apNoteService: ApNoteService, private apNoteService: ApNoteService,
private driveService: DriveService, private driveService: DriveService,
private downloadService: DownloadService, private downloadService: DownloadService,
private idService: IdService,
private queueLoggerService: QueueLoggerService, private queueLoggerService: QueueLoggerService,
) { ) {
this.logger = this.queueLoggerService.logger.createSubLogger('import-notes'); this.logger = this.queueLoggerService.logger.createSubLogger('import-notes');
} }
@bindThis @bindThis
private async uploadFiles(dir: string, user: MiUser) { private async uploadFiles(dir: string, user: MiUser, folder?: MiDriveFolder['id']) {
const fileList = fs.readdirSync(dir); const fileList = fs.readdirSync(dir);
for await (const file of fileList) { for await (const file of fileList) {
const name = `${dir}/${file}`; const name = `${dir}/${file}`;
if (fs.statSync(name).isDirectory()) { if (fs.statSync(name).isDirectory()) {
await this.uploadFiles(name, user); await this.uploadFiles(name, user, folder);
} else { } else {
const exists = await this.driveFilesRepository.findOneBy({ name: file, userId: user.id }); const exists = await this.driveFilesRepository.findOneBy({ name: file, userId: user.id, folderId: folder });
if (file.endsWith('.srt')) return; if (file.endsWith('.srt')) return;
@ -60,6 +65,7 @@ export class ImportNotesProcessorService {
user: user, user: user,
path: name, path: name,
name: file, name: file,
folderId: folder,
}); });
} }
} }
@ -68,7 +74,7 @@ export class ImportNotesProcessorService {
// Function was taken from Firefish and modified for our needs // Function was taken from Firefish and modified for our needs
@bindThis @bindThis
private async recreateChain(idField: string, replyField: string, arr: any[]): Promise<any[]> { private async recreateChain(idFieldPath: string[], replyFieldPath: string[], arr: any[], includeOrphans: boolean): Promise<any[]> {
type NotesMap = { type NotesMap = {
[id: string]: any; [id: string]: any;
}; };
@ -77,28 +83,42 @@ export class ImportNotesProcessorService {
const notesWaitingForParent: NotesMap = {}; const notesWaitingForParent: NotesMap = {};
for await (const note of arr) { for await (const note of arr) {
noteById[note[idField]] = note; const noteId = idFieldPath.reduce(
(obj, step) => obj[step],
note,
);
noteById[noteId] = note;
note.childNotes = []; note.childNotes = [];
const children = notesWaitingForParent[note[idField]]; const children = notesWaitingForParent[noteId];
if (children) { if (children) {
note.childNotes.push(...children); note.childNotes.push(...children);
delete notesWaitingForParent[noteId];
} }
if (note[replyField] == null) { const noteReplyId = replyFieldPath.reduce(
(obj, step) => obj[step],
note,
);
if (noteReplyId == null) {
notesTree.push(note); notesTree.push(note);
continue; continue;
} }
const parent = noteById[note[replyField]]; const parent = noteById[noteReplyId];
if (parent) { if (parent) {
parent.childNotes.push(note); parent.childNotes.push(note);
} else { } else {
notesWaitingForParent[note[replyField]] ||= []; notesWaitingForParent[noteReplyId] ||= [];
notesWaitingForParent[note[replyField]].push(note); notesWaitingForParent[noteReplyId].push(note);
} }
} }
if (includeOrphans) {
notesTree.push(...Object.values(notesWaitingForParent).flat(1));
}
return notesTree; return notesTree;
} }
@ -126,6 +146,12 @@ export class ImportNotesProcessorService {
return; return;
} }
let folder = await this.driveFoldersRepository.findOneBy({ name: 'Imports', userId: job.data.user.id });
if (folder == null) {
await this.driveFoldersRepository.insert({ id: this.idService.gen(), name: 'Imports', userId: job.data.user.id });
folder = await this.driveFoldersRepository.findOneBy({ name: 'Imports', userId: job.data.user.id });
}
const type = job.data.type; const type = job.data.type;
if (type === 'Twitter' || file.name.startsWith('twitter') && file.name.endsWith('.zip')) { if (type === 'Twitter' || file.name.startsWith('twitter') && file.name.endsWith('.zip')) {
@ -164,7 +190,7 @@ export class ImportNotesProcessorService {
const tweets = Object.keys(fakeWindow.window.YTD.tweets.part0).reduce((m, key, i, obj) => { const tweets = Object.keys(fakeWindow.window.YTD.tweets.part0).reduce((m, key, i, obj) => {
return m.concat(fakeWindow.window.YTD.tweets.part0[key].tweet); return m.concat(fakeWindow.window.YTD.tweets.part0[key].tweet);
}, []); }, []);
const processedTweets = await this.recreateChain("id_str", "in_reply_to_status_id_str", tweets); const processedTweets = await this.recreateChain(['id_str'], ['in_reply_to_status_id_str'], tweets, false);
this.queueService.createImportTweetsToDbJob(job.data.user, processedTweets, null); this.queueService.createImportTweetsToDbJob(job.data.user, processedTweets, null);
} finally { } finally {
cleanup(); cleanup();
@ -192,7 +218,12 @@ export class ImportNotesProcessorService {
ZipReader.withDestinationPath(outputPath).viaBuffer(await fs.promises.readFile(destPath)); ZipReader.withDestinationPath(outputPath).viaBuffer(await fs.promises.readFile(destPath));
const postsJson = fs.readFileSync(outputPath + '/your_activity_across_facebook/posts/your_posts__check_ins__photos_and_videos_1.json', 'utf-8'); const postsJson = fs.readFileSync(outputPath + '/your_activity_across_facebook/posts/your_posts__check_ins__photos_and_videos_1.json', 'utf-8');
const posts = JSON.parse(postsJson); const posts = JSON.parse(postsJson);
await this.uploadFiles(outputPath + '/your_activity_across_facebook/posts/media', user); const facebookFolder = await this.driveFoldersRepository.findOneBy({ name: 'Facebook', userId: job.data.user.id, parentId: folder?.id });
if (facebookFolder == null && folder) {
await this.driveFoldersRepository.insert({ id: this.idService.gen(), name: 'Facebook', userId: job.data.user.id, parentId: folder.id });
const createdFolder = await this.driveFoldersRepository.findOneBy({ name: 'Facebook', userId: job.data.user.id, parentId: folder.id });
if (createdFolder) await this.uploadFiles(outputPath + '/your_activity_across_facebook/posts/media', user, createdFolder.id);
}
this.queueService.createImportFBToDbJob(job.data.user, posts); this.queueService.createImportFBToDbJob(job.data.user, posts);
} finally { } finally {
cleanup(); cleanup();
@ -223,7 +254,12 @@ export class ImportNotesProcessorService {
if (isInstagram) { if (isInstagram) {
const postsJson = fs.readFileSync(outputPath + '/content/posts_1.json', 'utf-8'); const postsJson = fs.readFileSync(outputPath + '/content/posts_1.json', 'utf-8');
const posts = JSON.parse(postsJson); const posts = JSON.parse(postsJson);
await this.uploadFiles(outputPath + '/media/posts', user); const igFolder = await this.driveFoldersRepository.findOneBy({ name: 'Instagram', userId: job.data.user.id, parentId: folder?.id });
if (igFolder == null && folder) {
await this.driveFoldersRepository.insert({ id: this.idService.gen(), name: 'Instagram', userId: job.data.user.id, parentId: folder.id });
const createdFolder = await this.driveFoldersRepository.findOneBy({ name: 'Instagram', userId: job.data.user.id, parentId: folder.id });
if (createdFolder) await this.uploadFiles(outputPath + '/media/posts', user, createdFolder.id);
}
this.queueService.createImportIGToDbJob(job.data.user, posts); this.queueService.createImportIGToDbJob(job.data.user, posts);
} else if (isOutbox) { } else if (isOutbox) {
const actorJson = fs.readFileSync(outputPath + '/actor.json', 'utf-8'); const actorJson = fs.readFileSync(outputPath + '/actor.json', 'utf-8');
@ -232,12 +268,21 @@ export class ImportNotesProcessorService {
if (isPleroma) { if (isPleroma) {
const outboxJson = fs.readFileSync(outputPath + '/outbox.json', 'utf-8'); const outboxJson = fs.readFileSync(outputPath + '/outbox.json', 'utf-8');
const outbox = JSON.parse(outboxJson); const outbox = JSON.parse(outboxJson);
this.queueService.createImportPleroToDbJob(job.data.user, outbox.orderedItems.filter((x: any) => x.type === 'Create' && x.object.type === 'Note')); const processedToots = await this.recreateChain(['object', 'id'], ['object', 'inReplyTo'], outbox.orderedItems.filter((x: any) => x.type === 'Create' && x.object.type === 'Note'), true);
this.queueService.createImportPleroToDbJob(job.data.user, processedToots, null);
} else { } else {
const outboxJson = fs.readFileSync(outputPath + '/outbox.json', 'utf-8'); const outboxJson = fs.readFileSync(outputPath + '/outbox.json', 'utf-8');
const outbox = JSON.parse(outboxJson); const outbox = JSON.parse(outboxJson);
if (fs.existsSync(outputPath + '/media_attachments/files')) await this.uploadFiles(outputPath + '/media_attachments/files', user); let mastoFolder = await this.driveFoldersRepository.findOneBy({ name: 'Mastodon', userId: job.data.user.id, parentId: folder?.id });
this.queueService.createImportMastoToDbJob(job.data.user, outbox.orderedItems.filter((x: any) => x.type === 'Create' && x.object.type === 'Note')); if (mastoFolder == null && folder) {
await this.driveFoldersRepository.insert({ id: this.idService.gen(), name: 'Mastodon', userId: job.data.user.id, parentId: folder.id });
mastoFolder = await this.driveFoldersRepository.findOneBy({ name: 'Mastodon', userId: job.data.user.id, parentId: folder.id });
}
if (fs.existsSync(outputPath + '/media_attachments/files') && mastoFolder) {
await this.uploadFiles(outputPath + '/media_attachments/files', user, mastoFolder.id);
}
const processedToots = await this.recreateChain(['object', 'id'], ['object', 'inReplyTo'], outbox.orderedItems.filter((x: any) => x.type === 'Create' && x.object.type === 'Note'), true);
this.queueService.createImportMastoToDbJob(job.data.user, processedToots, null);
} }
} }
} finally { } finally {
@ -260,7 +305,7 @@ export class ImportNotesProcessorService {
const notesJson = fs.readFileSync(path, 'utf-8'); const notesJson = fs.readFileSync(path, 'utf-8');
const notes = JSON.parse(notesJson); const notes = JSON.parse(notesJson);
const processedNotes = await this.recreateChain("id", "replyId", notes); const processedNotes = await this.recreateChain(['id'], ['replyId'], notes, false);
this.queueService.createImportKeyNotesToDbJob(job.data.user, processedNotes, null); this.queueService.createImportKeyNotesToDbJob(job.data.user, processedNotes, null);
cleanup(); cleanup();
} }
@ -269,7 +314,7 @@ export class ImportNotesProcessorService {
} }
@bindThis @bindThis
public async processKeyNotesToDb(job: Bull.Job<DbKeyNoteImportToDbJobData>): Promise<void> { public async processKeyNotesToDb(job: Bull.Job<DbNoteWithParentImportToDbJobData>): Promise<void> {
const note = job.data.target; const note = job.data.target;
const user = await this.usersRepository.findOneBy({ id: job.data.user.id }); const user = await this.usersRepository.findOneBy({ id: job.data.user.id });
if (user == null) { if (user == null) {
@ -280,16 +325,25 @@ export class ImportNotesProcessorService {
const parentNote = job.data.note ? await this.notesRepository.findOneBy({ id: job.data.note }) : null; const parentNote = job.data.note ? await this.notesRepository.findOneBy({ id: job.data.note }) : null;
const folder = await this.driveFoldersRepository.findOneBy({ name: 'Imports', userId: job.data.user.id });
if (folder == null) return;
const files: MiDriveFile[] = []; const files: MiDriveFile[] = [];
const date = new Date(note.createdAt); const date = new Date(note.createdAt);
if (note.files && this.isIterable(note.files)) { if (note.files && this.isIterable(note.files)) {
let keyFolder = await this.driveFoldersRepository.findOneBy({ name: 'Misskey', userId: job.data.user.id, parentId: folder.id });
if (keyFolder == null) {
await this.driveFoldersRepository.insert({ id: this.idService.gen(), name: 'Misskey', userId: job.data.user.id, parentId: folder.id });
keyFolder = await this.driveFoldersRepository.findOneBy({ name: 'Misskey', userId: job.data.user.id, parentId: folder.id });
}
for await (const file of note.files) { for await (const file of note.files) {
const [filePath, cleanup] = await createTemp(); const [filePath, cleanup] = await createTemp();
const slashdex = file.url.lastIndexOf('/'); const slashdex = file.url.lastIndexOf('/');
const name = file.url.substring(slashdex + 1); const name = file.url.substring(slashdex + 1);
const exists = await this.driveFilesRepository.findOneBy({ name: name, userId: user.id }); const exists = await this.driveFilesRepository.findOneBy({ name: name, userId: user.id }) ?? await this.driveFilesRepository.findOneBy({ name: name, userId: user.id, folderId: keyFolder?.id });
if (!exists) { if (!exists) {
try { try {
@ -301,6 +355,7 @@ export class ImportNotesProcessorService {
user: user, user: user,
path: filePath, path: filePath,
name: name, name: name,
folderId: keyFolder?.id,
}); });
files.push(driveFile); files.push(driveFile);
} else { } else {
@ -316,27 +371,32 @@ export class ImportNotesProcessorService {
} }
@bindThis @bindThis
public async processMastoToDb(job: Bull.Job<DbNoteImportToDbJobData>): Promise<void> { public async processMastoToDb(job: Bull.Job<DbNoteWithParentImportToDbJobData>): Promise<void> {
const toot = job.data.target; const toot = job.data.target;
const user = await this.usersRepository.findOneBy({ id: job.data.user.id }); const user = await this.usersRepository.findOneBy({ id: job.data.user.id });
if (user == null) { if (user == null) {
return; return;
} }
if (toot.directMessage) return;
const date = new Date(toot.object.published); const date = new Date(toot.object.published);
let text = undefined; let text = undefined;
const files: MiDriveFile[] = []; const files: MiDriveFile[] = [];
let reply: MiNote | null = null; let reply: MiNote | null = null;
if (toot.object.inReplyTo != null) { if (toot.object.inReplyTo != null) {
const parentNote = job.data.note ? await this.notesRepository.findOneBy({ id: job.data.note }) : null;
if (parentNote) {
reply = parentNote;
} else {
try { try {
reply = await this.apNoteService.resolveNote(toot.object.inReplyTo); reply = await this.apNoteService.resolveNote(toot.object.inReplyTo);
} catch (error) { } catch (error) {
reply = null; reply = null;
} }
} }
}
if (toot.directMessage) return;
const hashtags = extractApHashtagObjects(toot.object.tag).map((x) => x.name).filter((x): x is string => x != null); const hashtags = extractApHashtagObjects(toot.object.tag).map((x) => x.name).filter((x): x is string => x != null);
@ -357,31 +417,40 @@ export class ImportNotesProcessorService {
} }
} }
await this.noteCreateService.import(user, { createdAt: date, text: text, files: files, apMentions: new Array(0), cw: toot.object.sensitive ? toot.object.summary : null, reply: reply }); const createdNote = await this.noteCreateService.import(user, { createdAt: date, text: text, files: files, apMentions: new Array(0), cw: toot.object.sensitive ? toot.object.summary : null, reply: reply });
if (toot.childNotes) this.queueService.createImportMastoToDbJob(user, toot.childNotes, createdNote.id);
} }
@bindThis @bindThis
public async processPleroToDb(job: Bull.Job<DbNoteImportToDbJobData>): Promise<void> { public async processPleroToDb(job: Bull.Job<DbNoteWithParentImportToDbJobData>): Promise<void> {
const post = job.data.target; const post = job.data.target;
const user = await this.usersRepository.findOneBy({ id: job.data.user.id }); const user = await this.usersRepository.findOneBy({ id: job.data.user.id });
if (user == null) { if (user == null) {
return; return;
} }
if (post.directMessage) return;
const date = new Date(post.object.published); const date = new Date(post.object.published);
let text = undefined; let text = undefined;
const files: MiDriveFile[] = []; const files: MiDriveFile[] = [];
let reply: MiNote | null = null; let reply: MiNote | null = null;
const folder = await this.driveFoldersRepository.findOneBy({ name: 'Imports', userId: job.data.user.id });
if (folder == null) return;
if (post.object.inReplyTo != null) { if (post.object.inReplyTo != null) {
const parentNote = job.data.note ? await this.notesRepository.findOneBy({ id: job.data.note }) : null;
if (parentNote) {
reply = parentNote;
} else {
try { try {
reply = await this.apNoteService.resolveNote(post.object.inReplyTo); reply = await this.apNoteService.resolveNote(post.object.inReplyTo);
} catch (error) { } catch (error) {
reply = null; reply = null;
} }
} }
}
if (post.directMessage) return;
const hashtags = extractApHashtagObjects(post.object.tag).map((x) => x.name).filter((x): x is string => x != null); const hashtags = extractApHashtagObjects(post.object.tag).map((x) => x.name).filter((x): x is string => x != null);
@ -392,12 +461,18 @@ export class ImportNotesProcessorService {
} }
if (post.object.attachment && this.isIterable(post.object.attachment)) { if (post.object.attachment && this.isIterable(post.object.attachment)) {
let pleroFolder = await this.driveFoldersRepository.findOneBy({ name: 'Pleroma', userId: job.data.user.id, parentId: folder.id });
if (pleroFolder == null) {
await this.driveFoldersRepository.insert({ id: this.idService.gen(), name: 'Pleroma', userId: job.data.user.id, parentId: folder.id });
pleroFolder = await this.driveFoldersRepository.findOneBy({ name: 'Pleroma', userId: job.data.user.id, parentId: folder.id });
}
for await (const file of post.object.attachment) { for await (const file of post.object.attachment) {
const slashdex = file.url.lastIndexOf('/'); const slashdex = file.url.lastIndexOf('/');
const name = file.url.substring(slashdex + 1); const name = file.url.substring(slashdex + 1);
const [filePath, cleanup] = await createTemp(); const [filePath, cleanup] = await createTemp();
const exists = await this.driveFilesRepository.findOneBy({ name: name, userId: user.id }); const exists = await this.driveFilesRepository.findOneBy({ name: name, userId: user.id }) ?? await this.driveFilesRepository.findOneBy({ name: name, userId: user.id, folderId: pleroFolder?.id });
if (!exists) { if (!exists) {
try { try {
@ -409,6 +484,7 @@ export class ImportNotesProcessorService {
user: user, user: user,
path: filePath, path: filePath,
name: name, name: name,
folderId: pleroFolder?.id,
}); });
files.push(driveFile); files.push(driveFile);
} else { } else {
@ -419,7 +495,8 @@ export class ImportNotesProcessorService {
} }
} }
await this.noteCreateService.import(user, { createdAt: date, text: text, files: files, apMentions: new Array(0), cw: post.object.sensitive ? post.object.summary : null, reply: reply }); const createdNote = await this.noteCreateService.import(user, { createdAt: date, text: text, files: files, apMentions: new Array(0), cw: post.object.sensitive ? post.object.summary : null, reply: reply });
if (post.childNotes) this.queueService.createImportPleroToDbJob(user, post.childNotes, createdNote.id);
} }
@bindThis @bindThis
@ -468,13 +545,16 @@ export class ImportNotesProcessorService {
} }
@bindThis @bindThis
public async processTwitterDb(job: Bull.Job<DbKeyNoteImportToDbJobData>): Promise<void> { public async processTwitterDb(job: Bull.Job<DbNoteWithParentImportToDbJobData>): Promise<void> {
const tweet = job.data.target; const tweet = job.data.target;
const user = await this.usersRepository.findOneBy({ id: job.data.user.id }); const user = await this.usersRepository.findOneBy({ id: job.data.user.id });
if (user == null) { if (user == null) {
return; return;
} }
const folder = await this.driveFoldersRepository.findOneBy({ name: 'Imports', userId: job.data.user.id });
if (folder == null) return;
const parentNote = job.data.note ? await this.notesRepository.findOneBy({ id: job.data.note }) : null; const parentNote = job.data.note ? await this.notesRepository.findOneBy({ id: job.data.note }) : null;
async function replaceTwitterUrls(full_text: string, urls: any) { async function replaceTwitterUrls(full_text: string, urls: any) {
@ -500,13 +580,19 @@ export class ImportNotesProcessorService {
const files: MiDriveFile[] = []; const files: MiDriveFile[] = [];
if (tweet.extended_entities && this.isIterable(tweet.extended_entities.media)) { if (tweet.extended_entities && this.isIterable(tweet.extended_entities.media)) {
let twitFolder = await this.driveFoldersRepository.findOneBy({ name: 'Twitter', userId: job.data.user.id, parentId: folder.id });
if (twitFolder == null) {
await this.driveFoldersRepository.insert({ id: this.idService.gen(), name: 'Twitter', userId: job.data.user.id, parentId: folder.id });
twitFolder = await this.driveFoldersRepository.findOneBy({ name: 'Twitter', userId: job.data.user.id, parentId: folder.id });
}
for await (const file of tweet.extended_entities.media) { for await (const file of tweet.extended_entities.media) {
if (file.video_info) { if (file.video_info) {
const [filePath, cleanup] = await createTemp(); const [filePath, cleanup] = await createTemp();
const slashdex = file.video_info.variants[0].url.lastIndexOf('/'); const slashdex = file.video_info.variants[0].url.lastIndexOf('/');
const name = file.video_info.variants[0].url.substring(slashdex + 1); const name = file.video_info.variants[0].url.substring(slashdex + 1);
const exists = await this.driveFilesRepository.findOneBy({ name: name, userId: user.id }); const exists = await this.driveFilesRepository.findOneBy({ name: name, userId: user.id }) ?? await this.driveFilesRepository.findOneBy({ name: name, userId: user.id, folderId: twitFolder?.id });
const videos = file.video_info.variants.filter((x: any) => x.content_type === 'video/mp4'); const videos = file.video_info.variants.filter((x: any) => x.content_type === 'video/mp4');
@ -520,6 +606,7 @@ export class ImportNotesProcessorService {
user: user, user: user,
path: filePath, path: filePath,
name: name, name: name,
folderId: twitFolder?.id,
}); });
files.push(driveFile); files.push(driveFile);
} else { } else {
@ -545,6 +632,7 @@ export class ImportNotesProcessorService {
user: user, user: user,
path: filePath, path: filePath,
name: name, name: name,
folderId: twitFolder?.id,
}); });
files.push(driveFile); files.push(driveFile);
} else { } else {

View file

@ -50,12 +50,12 @@ export type DbJobMap = {
exportUserLists: DbJobDataWithUser; exportUserLists: DbJobDataWithUser;
importAntennas: DBAntennaImportJobData; importAntennas: DBAntennaImportJobData;
importNotes: DbNoteImportJobData; importNotes: DbNoteImportJobData;
importTweetsToDb: DbKeyNoteImportToDbJobData; importTweetsToDb: DbNoteWithParentImportToDbJobData;
importIGToDb: DbNoteImportToDbJobData; importIGToDb: DbNoteImportToDbJobData;
importFBToDb: DbNoteImportToDbJobData; importFBToDb: DbNoteImportToDbJobData;
importMastoToDb: DbNoteImportToDbJobData; importMastoToDb: DbNoteWithParentImportToDbJobData;
importPleroToDb: DbNoteImportToDbJobData; importPleroToDb: DbNoteWithParentImportToDbJobData;
importKeyNotesToDb: DbKeyNoteImportToDbJobData; importKeyNotesToDb: DbNoteWithParentImportToDbJobData;
importFollowing: DbUserImportJobData; importFollowing: DbUserImportJobData;
importFollowingToDb: DbUserImportToDbJobData; importFollowingToDb: DbUserImportToDbJobData;
importMuting: DbUserImportJobData; importMuting: DbUserImportJobData;
@ -113,7 +113,7 @@ export type DbNoteImportToDbJobData = {
target: any; target: any;
}; };
export type DbKeyNoteImportToDbJobData = { export type DbNoteWithParentImportToDbJobData = {
user: ThinUser; user: ThinUser;
target: any; target: any;
note: MiNote['id'] | null; note: MiNote['id'] | null;

View file

@ -13,7 +13,7 @@ export const meta = {
prohibitMoved: true, prohibitMoved: true,
limit: { limit: {
duration: ms('1hour'), duration: ms('1hour'),
max: 5, max: 2,
}, },
errors: { errors: {

View file

@ -14,7 +14,7 @@ import { DI } from '@/di-symbols.js';
import { ApiError } from '../../error.js'; import { ApiError } from '../../error.js';
export const meta = { export const meta = {
tags: ["notes"], tags: ['notes'],
requireCredential: true, requireCredential: true,
@ -23,99 +23,99 @@ export const meta = {
max: 300, max: 300,
}, },
kind: "write:notes", kind: 'write:notes',
res: { res: {
type: "object", type: 'object',
optional: false, optional: false,
nullable: false, nullable: false,
properties: { properties: {
createdNote: { createdNote: {
type: "object", type: 'object',
optional: false, optional: false,
nullable: false, nullable: false,
ref: "Note", ref: 'Note',
}, },
}, },
}, },
errors: { errors: {
noSuchRenoteTarget: { noSuchRenoteTarget: {
message: "No such renote target.", message: 'No such renote target.',
code: "NO_SUCH_RENOTE_TARGET", code: 'NO_SUCH_RENOTE_TARGET',
id: "b5c90186-4ab0-49c8-9bba-a1f76c282ba4", id: 'b5c90186-4ab0-49c8-9bba-a1f76c282ba4',
}, },
cannotReRenote: { cannotReRenote: {
message: "You can not Renote a pure Renote.", message: 'You can not Renote a pure Renote.',
code: "CANNOT_RENOTE_TO_A_PURE_RENOTE", code: 'CANNOT_RENOTE_TO_A_PURE_RENOTE',
id: "fd4cc33e-2a37-48dd-99cc-9b806eb2031a", id: 'fd4cc33e-2a37-48dd-99cc-9b806eb2031a',
}, },
noSuchReplyTarget: { noSuchReplyTarget: {
message: "No such reply target.", message: 'No such reply target.',
code: "NO_SUCH_REPLY_TARGET", code: 'NO_SUCH_REPLY_TARGET',
id: "749ee0f6-d3da-459a-bf02-282e2da4292c", id: '749ee0f6-d3da-459a-bf02-282e2da4292c',
}, },
cannotReplyToPureRenote: { cannotReplyToPureRenote: {
message: "You can not reply to a pure Renote.", message: 'You can not reply to a pure Renote.',
code: "CANNOT_REPLY_TO_A_PURE_RENOTE", code: 'CANNOT_REPLY_TO_A_PURE_RENOTE',
id: "3ac74a84-8fd5-4bb0-870f-01804f82ce15", id: '3ac74a84-8fd5-4bb0-870f-01804f82ce15',
}, },
cannotCreateAlreadyExpiredPoll: { cannotCreateAlreadyExpiredPoll: {
message: "Poll is already expired.", message: 'Poll is already expired.',
code: "CANNOT_CREATE_ALREADY_EXPIRED_POLL", code: 'CANNOT_CREATE_ALREADY_EXPIRED_POLL',
id: "04da457d-b083-4055-9082-955525eda5a5", id: '04da457d-b083-4055-9082-955525eda5a5',
}, },
noSuchChannel: { noSuchChannel: {
message: "No such channel.", message: 'No such channel.',
code: "NO_SUCH_CHANNEL", code: 'NO_SUCH_CHANNEL',
id: "b1653923-5453-4edc-b786-7c4f39bb0bbb", id: 'b1653923-5453-4edc-b786-7c4f39bb0bbb',
}, },
youHaveBeenBlocked: { youHaveBeenBlocked: {
message: "You have been blocked by this user.", message: 'You have been blocked by this user.',
code: "YOU_HAVE_BEEN_BLOCKED", code: 'YOU_HAVE_BEEN_BLOCKED',
id: "b390d7e1-8a5e-46ed-b625-06271cafd3d3", id: 'b390d7e1-8a5e-46ed-b625-06271cafd3d3',
}, },
accountLocked: { accountLocked: {
message: "You migrated. Your account is now locked.", message: 'You migrated. Your account is now locked.',
code: "ACCOUNT_LOCKED", code: 'ACCOUNT_LOCKED',
id: "d390d7e1-8a5e-46ed-b625-06271cafd3d3", id: 'd390d7e1-8a5e-46ed-b625-06271cafd3d3',
}, },
needsEditId: { needsEditId: {
message: "You need to specify `editId`.", message: 'You need to specify `editId`.',
code: "NEEDS_EDIT_ID", code: 'NEEDS_EDIT_ID',
id: "d697edc8-8c73-4de8-bded-35fd198b79e5", id: 'd697edc8-8c73-4de8-bded-35fd198b79e5',
}, },
noSuchNote: { noSuchNote: {
message: "No such note.", message: 'No such note.',
code: "NO_SUCH_NOTE", code: 'NO_SUCH_NOTE',
id: "eef6c173-3010-4a23-8674-7c4fcaeba719", id: 'eef6c173-3010-4a23-8674-7c4fcaeba719',
}, },
youAreNotTheAuthor: { youAreNotTheAuthor: {
message: "You are not the author of this note.", message: 'You are not the author of this note.',
code: "YOU_ARE_NOT_THE_AUTHOR", code: 'YOU_ARE_NOT_THE_AUTHOR',
id: "c6e61685-411d-43d0-b90a-a448d2539001", id: 'c6e61685-411d-43d0-b90a-a448d2539001',
}, },
cannotPrivateRenote: { cannotPrivateRenote: {
message: "You can not perform a private renote.", message: 'You can not perform a private renote.',
code: "CANNOT_PRIVATE_RENOTE", code: 'CANNOT_PRIVATE_RENOTE',
id: "19a50f1c-84fa-4e33-81d3-17834ccc0ad8", id: '19a50f1c-84fa-4e33-81d3-17834ccc0ad8',
}, },
notLocalUser: { notLocalUser: {
message: "You are not a local user.", message: 'You are not a local user.',
code: "NOT_LOCAL_USER", code: 'NOT_LOCAL_USER',
id: "b907f407-2aa0-4283-800b-a2c56290b822", id: 'b907f407-2aa0-4283-800b-a2c56290b822',
}, },
cannotRenoteOutsideOfChannel: { cannotRenoteOutsideOfChannel: {
@ -127,60 +127,63 @@ export const meta = {
} as const; } as const;
export const paramDef = { export const paramDef = {
type: "object", type: 'object',
properties: { properties: {
editId: { type: "string", format: "misskey:id" }, editId: { type: 'string', format: 'misskey:id' },
visibility: { type: "string", enum: ['public', 'home', 'followers', 'specified'], default: "public" }, visibility: { type: 'string', enum: ['public', 'home', 'followers', 'specified'], default: 'public' },
visibleUserIds: { visibleUserIds: {
type: "array", type: 'array',
uniqueItems: true, uniqueItems: true,
items: { items: {
type: "string", type: 'string',
format: "misskey:id", format: 'misskey:id',
}, },
}, },
text: { type: "string", maxLength: MAX_NOTE_TEXT_LENGTH, nullable: true }, cw: { type: 'string', nullable: true, minLength: 1, maxLength: 250 },
cw: { type: "string", nullable: true, minLength: 1, maxLength: 250 }, localOnly: { type: 'boolean', default: false },
localOnly: { type: "boolean", default: false }, reactionAcceptance: { type: 'string', nullable: true, enum: [null, 'likeOnly', 'likeOnlyForRemote', 'nonSensitiveOnly', 'nonSensitiveOnlyForLocalLikeOnlyForRemote'], default: null },
noExtractMentions: { type: "boolean", default: false }, noExtractMentions: { type: 'boolean', default: false },
noExtractHashtags: { type: "boolean", default: false }, noExtractHashtags: { type: 'boolean', default: false },
noExtractEmojis: { type: "boolean", default: false }, noExtractEmojis: { type: 'boolean', default: false },
replyId: { type: 'string', format: 'misskey:id', nullable: true },
renoteId: { type: 'string', format: 'misskey:id', nullable: true },
channelId: { type: 'string', format: 'misskey:id', nullable: true },
text: {
type: 'string',
minLength: 1,
maxLength: MAX_NOTE_TEXT_LENGTH,
nullable: true,
},
fileIds: { fileIds: {
type: "array", type: 'array',
uniqueItems: true, uniqueItems: true,
minItems: 1, minItems: 1,
maxItems: 16, maxItems: 16,
items: { type: "string", format: "misskey:id" }, items: { type: 'string', format: 'misskey:id' },
}, },
mediaIds: { mediaIds: {
deprecated: true, type: 'array',
description:
"Use `fileIds` instead. If both are specified, this property is discarded.",
type: "array",
uniqueItems: true, uniqueItems: true,
minItems: 1, minItems: 1,
maxItems: 16, maxItems: 16,
items: { type: "string", format: "misskey:id" }, items: { type: 'string', format: 'misskey:id' },
}, },
replyId: { type: "string", format: "misskey:id", nullable: true },
renoteId: { type: "string", format: "misskey:id", nullable: true },
channelId: { type: "string", format: "misskey:id", nullable: true },
poll: { poll: {
type: "object", type: 'object',
nullable: true, nullable: true,
properties: { properties: {
choices: { choices: {
type: "array", type: 'array',
uniqueItems: true, uniqueItems: true,
minItems: 2, minItems: 2,
maxItems: 10, maxItems: 10,
items: { type: "string", minLength: 1, maxLength: 50 }, items: { type: 'string', minLength: 1, maxLength: 50 },
}, },
multiple: { type: "boolean", default: false }, multiple: { type: 'boolean' },
expiresAt: { type: "integer", nullable: true }, expiresAt: { type: 'integer', nullable: true },
expiredAfter: { type: "integer", nullable: true, minimum: 1 }, expiredAfter: { type: 'integer', nullable: true, minimum: 1 },
}, },
required: ["choices"], required: ['choices'],
}, },
}, },
anyOf: [ anyOf: [
@ -188,32 +191,32 @@ export const paramDef = {
// (re)note with text, files and poll are optional // (re)note with text, files and poll are optional
properties: { properties: {
text: { text: {
type: "string", type: 'string',
minLength: 1, minLength: 1,
maxLength: MAX_NOTE_TEXT_LENGTH, maxLength: MAX_NOTE_TEXT_LENGTH,
nullable: false, nullable: false,
}, },
}, },
required: ["text"], required: ['text'],
}, },
{ {
// (re)note with files, text and poll are optional // (re)note with files, text and poll are optional
required: ["fileIds"], required: ['fileIds'],
}, },
{ {
// (re)note with files, text and poll are optional // (re)note with files, text and poll are optional
required: ["mediaIds"], required: ['mediaIds'],
}, },
{ {
// (re)note with poll, text and files are optional // (re)note with poll, text and files are optional
properties: { properties: {
poll: { type: "object", nullable: false }, poll: { type: 'object', nullable: false },
}, },
required: ["poll"], required: ['poll'],
}, },
{ {
// pure renote // pure renote
required: ["renoteId"], required: ['renoteId'],
}, },
], ],
} as const; } as const;

View file

@ -139,7 +139,9 @@ export class ClientServerService {
'type': 'image/png', 'type': 'image/png',
'purpose': 'maskable', 'purpose': 'maskable',
}, { }, {
'src': '/static-assets/splash.png', // 空文字列の場合右辺を使いたいため
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
'src': instance.app512IconUrl || '/static-assets/icons/512.png',
'sizes': '300x300', 'sizes': '300x300',
'type': 'image/png', 'type': 'image/png',
'purpose': 'any', 'purpose': 'any',

View file

@ -39,9 +39,7 @@ export async function mainBoot() {
let reloadDialogShowing = false; let reloadDialogShowing = false;
stream.on('_disconnected_', async () => { stream.on('_disconnected_', async () => {
if (defaultStore.state.serverDisconnectedBehavior === 'reload') { if (defaultStore.state.serverDisconnectedBehavior === 'dialog') {
location.reload();
} else if (defaultStore.state.serverDisconnectedBehavior === 'dialog') {
if (reloadDialogShowing) return; if (reloadDialogShowing) return;
reloadDialogShowing = true; reloadDialogShowing = true;
const { canceled } = await confirm({ const { canceled } = await confirm({

View file

@ -57,6 +57,48 @@ const avatarUrl = computed(() => defaultStore.state.disableShowingAnimatedImages
} }
} }
.root + .root {
position: relative;
margin-inline: -20px 0;
box-shadow: -4px 0 0 var(--panel), -15px 0 15px var(--panel);
overflow: clip;
isolation: isolate;
&::before {
content: "";
position: absolute;
inset: 0;
background: var(--panel);
z-index: -1;
}
&::after {
content: "";
position: absolute;
inset: 0;
background: var(--panel);
z-index: -1;
background: inherit;
}
span {
display: inline-block;
white-space: nowrap;
max-width: 3em;
mask: linear-gradient(to right, #000 20%, rgba(0, 0, 0, 0.4));
}
+ .root {
margin-inline: -10px 0;
padding-inline-end: 0;
box-shadow: -4px 0 0 var(--panel);
span {
display: none;
}
}
}
.icon { .icon {
width: 1.5em; width: 1.5em;
height: 1.5em; height: 1.5em;

View file

@ -127,9 +127,8 @@ SPDX-License-Identifier: AGPL-3.0-only
ref="quoteButton" ref="quoteButton"
:class="$style.footerButton" :class="$style.footerButton"
class="_button" class="_button"
:style="quoted ? 'color: var(--accent) !important;' : ''"
v-on:click.stop v-on:click.stop
@mousedown="quoted ? undoQuote(appearNote) : quote()" @mousedown="quote()"
> >
<i class="ph-quotes ph-bold ph-lg"></i> <i class="ph-quotes ph-bold ph-lg"></i>
</button> </button>
@ -226,8 +225,11 @@ const currentClip = inject<Ref<Misskey.entities.Clip> | null>('currentClip', nul
let note = $ref(deepClone(props.note)); let note = $ref(deepClone(props.note));
function noteclick(id: string) { function noteclick(id: string) {
const selection = document.getSelection();
if (selection?.toString().length === 0) {
router.push(`/notes/${id}`); router.push(`/notes/${id}`);
} }
}
// plugin // plugin
if (noteViewInterruptors.length > 0) { if (noteViewInterruptors.length > 0) {
@ -278,14 +280,13 @@ const isLong = shouldCollapsed(appearNote, urls ?? []);
const collapsed = ref(appearNote.cw == null && isLong); const collapsed = ref(appearNote.cw == null && isLong);
const isDeleted = ref(false); const isDeleted = ref(false);
const renoted = ref(false); const renoted = ref(false);
const quoted = ref(false);
const muted = ref($i ? checkWordMute(appearNote, $i, $i.mutedWords) : false); const muted = ref($i ? checkWordMute(appearNote, $i, $i.mutedWords) : false);
const translation = ref<any>(null); const translation = ref<any>(null);
const translating = ref(false); const translating = ref(false);
const showTicker = (defaultStore.state.instanceTicker === 'always') || (defaultStore.state.instanceTicker === 'remote' && appearNote.user.instance); const showTicker = (defaultStore.state.instanceTicker === 'always') || (defaultStore.state.instanceTicker === 'remote' && appearNote.user.instance);
const canRenote = computed(() => ['public', 'home'].includes(appearNote.visibility) || (appearNote.visibility === 'followers' && appearNote.userId === $i.id)); const canRenote = computed(() => ['public', 'home'].includes(appearNote.visibility) || (appearNote.visibility === 'followers' && appearNote.userId === $i.id));
let renoteCollapsed = $ref(defaultStore.state.collapseRenotes && isRenote && (($i && ($i.id === note.userId || $i.id === appearNote.userId)) || (appearNote.myReaction != null))); let renoteCollapsed = $ref(defaultStore.state.collapseRenotes && isRenote && (($i && ($i.id === note.userId || $i.id === appearNote.userId)) || (appearNote.myReaction != null)));
const defaultLike = computed(() => defaultStore.state.like !== '❤️' ? defaultStore.state.like : null); const defaultLike = computed(() => defaultStore.state.like ? defaultStore.state.like : null);
const keymap = { const keymap = {
'r': () => reply(true), 'r': () => reply(true),
@ -364,15 +365,6 @@ if (!props.mock) {
}).then((res) => { }).then((res) => {
renoted.value = res.length > 0; renoted.value = res.length > 0;
}); });
os.api("notes/renotes", {
noteId: appearNote.id,
userId: $i.id,
limit: 1,
quote: true,
}).then((res) => {
quoted.value = res.length > 0;
});
} }
} }
@ -467,7 +459,6 @@ function quote() {
os.popup(MkRippleEffect, { x, y }, {}, 'end'); os.popup(MkRippleEffect, { x, y }, {}, 'end');
} }
quoted.value = res.length > 0;
os.toast(i18n.ts.quoted); os.toast(i18n.ts.quoted);
}); });
}); });
@ -490,7 +481,6 @@ function quote() {
os.popup(MkRippleEffect, { x, y }, {}, 'end'); os.popup(MkRippleEffect, { x, y }, {}, 'end');
} }
quoted.value = res.length > 0;
os.toast(i18n.ts.quoted); os.toast(i18n.ts.quoted);
}); });
}); });
@ -603,26 +593,6 @@ function undoRenote(note) : void {
} }
} }
function undoQuote(note) : void {
if (props.mock) {
return;
}
os.api("notes/unrenote", {
noteId: note.id,
quote: true
});
os.toast(i18n.ts.rmquote);
quoted.value = false;
const el = quoteButton.value as HTMLElement | null | undefined;
if (el) {
const rect = el.getBoundingClientRect();
const x = rect.left + (el.offsetWidth / 2);
const y = rect.top + (el.offsetHeight / 2);
os.popup(MkRippleEffect, { x, y }, {}, 'end');
}
}
function onContextmenu(ev: MouseEvent): void { function onContextmenu(ev: MouseEvent): void {
if (props.mock) { if (props.mock) {
return; return;

View file

@ -137,8 +137,7 @@ SPDX-License-Identifier: AGPL-3.0-only
ref="quoteButton" ref="quoteButton"
class="_button" class="_button"
:class="$style.noteFooterButton" :class="$style.noteFooterButton"
:style="quoted ? 'color: var(--accent) !important;' : ''" @mousedown="quote()"
@mousedown="quoted ? undoQuote() : quote()"
> >
<i class="ph-quotes ph-bold ph-lg"></i> <i class="ph-quotes ph-bold ph-lg"></i>
</button> </button>
@ -310,7 +309,6 @@ const isMyRenote = $i && ($i.id === note.userId);
const showContent = ref(false); const showContent = ref(false);
const isDeleted = ref(false); const isDeleted = ref(false);
const renoted = ref(false); const renoted = ref(false);
const quoted = ref(false);
const muted = ref($i ? checkWordMute(appearNote, $i, $i.mutedWords) : false); const muted = ref($i ? checkWordMute(appearNote, $i, $i.mutedWords) : false);
const translation = ref(null); const translation = ref(null);
const translating = ref(false); const translating = ref(false);
@ -323,7 +321,7 @@ const conversation = ref<Misskey.entities.Note[]>([]);
const replies = ref<Misskey.entities.Note[]>([]); const replies = ref<Misskey.entities.Note[]>([]);
const quotes = ref<Misskey.entities.Note[]>([]); const quotes = ref<Misskey.entities.Note[]>([]);
const canRenote = computed(() => ['public', 'home'].includes(appearNote.visibility) || appearNote.userId === $i.id); const canRenote = computed(() => ['public', 'home'].includes(appearNote.visibility) || appearNote.userId === $i.id);
const defaultLike = computed(() => defaultStore.state.like !== '❤️' ? defaultStore.state.like : null); const defaultLike = computed(() => defaultStore.state.like ? defaultStore.state.like : null);
watch(() => props.expandAllCws, (expandAllCws) => { watch(() => props.expandAllCws, (expandAllCws) => {
if (expandAllCws !== showContent.value) showContent.value = expandAllCws; if (expandAllCws !== showContent.value) showContent.value = expandAllCws;
@ -337,15 +335,6 @@ if ($i) {
}).then((res) => { }).then((res) => {
renoted.value = res.length > 0; renoted.value = res.length > 0;
}); });
os.api("notes/renotes", {
noteId: appearNote.id,
userId: $i.id,
limit: 1,
quote: true,
}).then((res) => {
quoted.value = res.length > 0;
});
} }
const keymap = { const keymap = {
@ -511,7 +500,6 @@ function quote() {
os.popup(MkRippleEffect, { x, y }, {}, 'end'); os.popup(MkRippleEffect, { x, y }, {}, 'end');
} }
quoted.value = res.length > 0;
os.toast(i18n.ts.quoted); os.toast(i18n.ts.quoted);
}); });
}); });
@ -534,7 +522,6 @@ function quote() {
os.popup(MkRippleEffect, { x, y }, {}, 'end'); os.popup(MkRippleEffect, { x, y }, {}, 'end');
} }
quoted.value = res.length > 0;
os.toast(i18n.ts.quoted); os.toast(i18n.ts.quoted);
}); });
}); });
@ -625,23 +612,6 @@ function undoRenote() : void {
} }
} }
function undoQuote() : void {
os.api("notes/unrenote", {
noteId: appearNote.id,
quote: true
});
os.toast(i18n.ts.rmquote);
quoted.value = false;
const el = quoteButton.value as HTMLElement | null | undefined;
if (el) {
const rect = el.getBoundingClientRect();
const x = rect.left + (el.offsetWidth / 2);
const y = rect.top + (el.offsetHeight / 2);
os.popup(MkRippleEffect, { x, y }, {}, 'end');
}
}
function onContextmenu(ev: MouseEvent): void { function onContextmenu(ev: MouseEvent): void {
const isLink = (el: HTMLElement) => { const isLink = (el: HTMLElement) => {
if (el.tagName === 'A') return true; if (el.tagName === 'A') return true;

View file

@ -11,7 +11,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<div> <div>
<p v-if="note.cw != null" :class="$style.cw"> <p v-if="note.cw != null" :class="$style.cw">
<Mfm v-if="note.cw != ''" style="margin-right: 8px;" :text="note.cw" :author="note.user" :nyaize="'respect'" :emojiUrls="note.emojis"/> <Mfm v-if="note.cw != ''" style="margin-right: 8px;" :text="note.cw" :author="note.user" :nyaize="'respect'" :emojiUrls="note.emojis"/>
<MkCwButton v-model="showContent" :note="note"/> <MkCwButton v-model="showContent" :note="note" v-on:click.stop/>
</p> </p>
<div v-show="note.cw == null || showContent"> <div v-show="note.cw == null || showContent">
<MkSubNoteContent :hideFiles="hideFiles" :class="$style.text" :note="note"/> <MkSubNoteContent :hideFiles="hideFiles" :class="$style.text" :note="note"/>

View file

@ -41,8 +41,7 @@ SPDX-License-Identifier: AGPL-3.0-only
ref="quoteButton" ref="quoteButton"
class="_button" class="_button"
:class="$style.noteFooterButton" :class="$style.noteFooterButton"
:style="quoted ? 'color: var(--accent) !important;' : ''" @mousedown="quote()"
@mousedown="quoted ? undoQuote() : quote()"
> >
<i class="ph-quotes ph-bold ph-lg"></i> <i class="ph-quotes ph-bold ph-lg"></i>
</button> </button>
@ -125,7 +124,6 @@ const translation = ref<any>(null);
const translating = ref(false); const translating = ref(false);
const isDeleted = ref(false); const isDeleted = ref(false);
const renoted = ref(false); const renoted = ref(false);
const quoted = ref(false);
const reactButton = shallowRef<HTMLElement>(); const reactButton = shallowRef<HTMLElement>();
const renoteButton = shallowRef<HTMLElement>(); const renoteButton = shallowRef<HTMLElement>();
const quoteButton = shallowRef<HTMLElement>(); const quoteButton = shallowRef<HTMLElement>();
@ -133,7 +131,7 @@ const menuButton = shallowRef<HTMLElement>();
const likeButton = shallowRef<HTMLElement>(); const likeButton = shallowRef<HTMLElement>();
let appearNote = $computed(() => isRenote ? props.note.renote as Misskey.entities.Note : props.note); let appearNote = $computed(() => isRenote ? props.note.renote as Misskey.entities.Note : props.note);
const defaultLike = computed(() => defaultStore.state.like !== '❤️' ? defaultStore.state.like : null); const defaultLike = computed(() => defaultStore.state.like ? defaultStore.state.like : null);
const isRenote = ( const isRenote = (
props.note.renote != null && props.note.renote != null &&
@ -156,15 +154,6 @@ if ($i) {
}).then((res) => { }).then((res) => {
renoted.value = res.length > 0; renoted.value = res.length > 0;
}); });
os.api("notes/renotes", {
noteId: appearNote.id,
userId: $i.id,
limit: 1,
quote: true,
}).then((res) => {
quoted.value = res.length > 0;
});
} }
function focus() { function focus() {
@ -255,23 +244,6 @@ function undoRenote() : void {
} }
} }
function undoQuote() : void {
os.api("notes/unrenote", {
noteId: appearNote.id,
quote: true
});
os.toast(i18n.ts.rmquote);
quoted.value = false;
const el = quoteButton.value as HTMLElement | null | undefined;
if (el) {
const rect = el.getBoundingClientRect();
const x = rect.left + (el.offsetWidth / 2);
const y = rect.top + (el.offsetHeight / 2);
os.popup(MkRippleEffect, { x, y }, {}, 'end');
}
}
let showContent = $ref(false); let showContent = $ref(false);
watch(() => props.expandAllCws, (expandAllCws) => { watch(() => props.expandAllCws, (expandAllCws) => {
@ -342,7 +314,6 @@ function quote() {
os.popup(MkRippleEffect, { x, y }, {}, 'end'); os.popup(MkRippleEffect, { x, y }, {}, 'end');
} }
quoted.value = res.length > 0;
os.toast(i18n.ts.quoted); os.toast(i18n.ts.quoted);
}); });
}); });
@ -365,7 +336,6 @@ function quote() {
os.popup(MkRippleEffect, { x, y }, {}, 'end'); os.popup(MkRippleEffect, { x, y }, {}, 'end');
} }
quoted.value = res.length > 0;
os.toast(i18n.ts.quoted); os.toast(i18n.ts.quoted);
}); });
}); });

View file

@ -17,6 +17,8 @@ SPDX-License-Identifier: AGPL-3.0-only
</ul> </ul>
<p v-if="!readOnly" :class="$style.info"> <p v-if="!readOnly" :class="$style.info">
<span>{{ i18n.t('_poll.totalVotes', { n: total }) }}</span> <span>{{ i18n.t('_poll.totalVotes', { n: total }) }}</span>
<span v-if="note.poll.multiple"> · </span>
<span v-if="note.poll.multiple">{{ i18n.ts._poll.multiple }}</span>
<span> · </span> <span> · </span>
<a v-if="!closed && !isVoted" style="color: inherit;" @click="showResult = !showResult">{{ showResult ? i18n.ts._poll.vote : i18n.ts._poll.showResult }}</a> <a v-if="!closed && !isVoted" style="color: inherit;" @click="showResult = !showResult">{{ showResult ? i18n.ts._poll.vote : i18n.ts._poll.showResult }}</a>
<span v-if="isVoted">{{ i18n.ts._poll.voted }}</span> <span v-if="isVoted">{{ i18n.ts._poll.voted }}</span>
@ -78,12 +80,19 @@ const vote = async (id) => {
pleaseLogin(); pleaseLogin();
if (props.readOnly || closed.value || isVoted.value) return; if (props.readOnly || closed.value || isVoted.value) return;
if (!props.note.poll.multiple) {
const { canceled } = await os.confirm({ const { canceled } = await os.confirm({
type: 'question', type: 'question',
text: i18n.t('voteConfirm', { choice: props.note.poll.choices[id].text }), text: i18n.t('voteConfirm', { choice: props.note.poll.choices[id].text }),
}); });
if (canceled) return; if (canceled) return;
} else {
const { canceled } = await os.confirm({
type: 'question',
text: i18n.t('voteConfirmMulti', { choice: props.note.poll.choices[id].text }),
});
if (canceled) return;
}
await os.api('notes/polls/vote', { await os.api('notes/polls/vote', {
noteId: props.note.id, noteId: props.note.id,

View file

@ -931,8 +931,8 @@ onMounted(() => {
poll = { poll = {
choices: init.poll.choices.map(x => x.text), choices: init.poll.choices.map(x => x.text),
multiple: init.poll.multiple, multiple: init.poll.multiple,
expiresAt: init.poll.expiresAt, expiresAt: init.poll.expiresAt ? new Date(init.poll.expiresAt).getTime().toString() : null,
expiredAfter: init.poll.expiredAfter, expiredAfter: init.poll.expiredAfter ? new Date(init.poll.expiredAfter).getTime().toString() : null,
}; };
} }
visibility = init.visibility; visibility = init.visibility;

View file

@ -16,7 +16,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkLoading v-if="translating" mini/> <MkLoading v-if="translating" mini/>
<div v-else> <div v-else>
<b>{{ i18n.t('translatedFrom', { x: translation.sourceLang }) }}: </b> <b>{{ i18n.t('translatedFrom', { x: translation.sourceLang }) }}: </b>
<Mfm :text="translation.text" :author="note.user" :nyaize="'account'" :emojiUrls="note.emojis"/> <Mfm :text="translation.text" :author="note.user" :nyaize="'respect'" :emojiUrls="note.emojis"/>
</div> </div>
</div> </div>
<MkA v-if="note.renoteId" :class="$style.rp" :to="`/notes/${note.renoteId}`" v-on:click.stop>RN: ...</MkA> <MkA v-if="note.renoteId" :class="$style.rp" :to="`/notes/${note.renoteId}`" v-on:click.stop>RN: ...</MkA>
@ -63,8 +63,11 @@ const props = defineProps<{
const router = useRouter(); const router = useRouter();
function noteclick(id: string) { function noteclick(id: string) {
const selection = document.getSelection();
if (selection?.toString().length === 0) {
router.push(`/notes/${id}`); router.push(`/notes/${id}`);
} }
}
const parsed = $computed(() => props.note.text ? mfm.parse(props.note.text) : null); const parsed = $computed(() => props.note.text ? mfm.parse(props.note.text) : null);
const animated = $computed(() => parsed ? checkAnimationFromMfm(parsed) : null); const animated = $computed(() => parsed ? checkAnimationFromMfm(parsed) : null);

View file

@ -28,9 +28,20 @@ SPDX-License-Identifier: AGPL-3.0-only
<div :class="$style.username"><MkAcct :user="user"/></div> <div :class="$style.username"><MkAcct :user="user"/></div>
</div> </div>
<div :class="$style.description"> <div :class="$style.description">
<Mfm v-if="user.description" :class="$style.mfm" :text="user.description" :author="user"/> <Mfm v-if="user.description" :nyaize="false" :class="$style.mfm" :text="user.description" :author="user"/>
<div v-else style="opacity: 0.7;">{{ i18n.ts.noAccountDescription }}</div> <div v-else style="opacity: 0.7;">{{ i18n.ts.noAccountDescription }}</div>
</div> </div>
<div v-if="user.fields.length > 0" :class="$style.fields">
<dl v-for="(field, i) in user.fields" :key="i" :class="$style.field">
<dt :class="$style.fieldname">
<Mfm :text="field.name" :nyaize="false" :plain="true" :colored="false"/>
</dt>
<dd :class="$style.fieldvalue">
<Mfm :text="field.value" :nyaize="false" :author="user" :colored="false"/>
<i v-if="user.verifiedLinks.includes(field.value)" v-tooltip:dialog="i18n.ts.verifiedLink" class="ph-seal-check ph-bold ph-lg" :class="$style.verifiedLink"></i>
</dd>
</dl>
</div>
<div :class="$style.status"> <div :class="$style.status">
<div :class="$style.statusItem"> <div :class="$style.statusItem">
<div :class="$style.statusItemLabel">{{ i18n.ts.notes }}</div> <div :class="$style.statusItemLabel">{{ i18n.ts.notes }}</div>
@ -221,6 +232,48 @@ onMounted(() => {
border-bottom: solid 1px var(--divider); border-bottom: solid 1px var(--divider);
} }
.fields {
font-size: 0.8em;
padding: 16px;
border-top: solid 1px var(--divider);
border-bottom: solid 1px var(--divider);
}
.field {
display: flex;
padding: 0;
margin: 0;
&:not(:last-child) {
margin-bottom: 8px;
}
:deep(span) {
white-space: nowrap !important;
}
}
.fieldvalue {
width: 70%;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
word-wrap: nowrap;
margin: 0;
}
.fieldname {
width: 100px;
max-height: 45px;
overflow: hidden;
white-space: nowrap;
display: inline;
text-overflow: ellipsis;
font-weight: bold;
text-align: center;
padding-inline-end: 10px;
}
.mfm { .mfm {
display: -webkit-box; display: -webkit-box;
-webkit-line-clamp: 5; -webkit-line-clamp: 5;

View file

@ -65,6 +65,13 @@ const props = defineProps<{
edit: boolean; edit: boolean;
}>(); }>();
// This will not be available for now as I don't think this is needed
// const notesSearchAvailable = (($i == null && instance.policies.canSearchNotes) || ($i != null && $i.policies.canSearchNotes));
/* if (!notesSearchAvailable) {
const wid = widgetDefs.findIndex(widget => widget === 'search');
widgetDefs.splice(wid, 1);
} */
const emit = defineEmits<{ const emit = defineEmits<{
(ev: 'updateWidgets', widgets: Widget[]): void; (ev: 'updateWidgets', widgets: Widget[]): void;
(ev: 'addWidget', widget: Widget): void; (ev: 'addWidget', widget: Widget): void;

View file

@ -0,0 +1,30 @@
<template>
<MkWindow ref="window" :initialWidth="600" :initialHeight="450" :canResize="true" @closed="emit('closed')">
<template #header>
<i class="ph-magnifying-glass ph-bold ph-lg" style="margin-right: 0.5em;"></i>
<b>Result</b>
</template>
<MkNotes :key="props.noteKey" :pagination="props.notePagination"/>
</MkWindow>
</template>
<script lang="ts" setup>
import { } from 'vue';
import type { Paging } from '@/components/MkPagination.vue';
import MkNotes from '@/components/MkNotes.vue';
import MkWindow from '@/components/MkWindow.vue';
const props = defineProps<{
noteKey: string | number | symbol | undefined;
notePagination: Paging;
}>();
const emit = defineEmits<{
(ev: 'closed'): void;
}>();
</script>
<style lang="scss" module>
</style>

View file

@ -82,7 +82,13 @@ export default function(props: MfmProps) {
res.push(t); res.push(t);
} }
res.shift(); res.shift();
// Don't wrap whitespaces in a span
if (text === ' ') {
return res; return res;
}
return h('span', res);
} else { } else {
return [text.replace(/\n/g, ' ')]; return [text.replace(/\n/g, ' ')];
} }

View file

@ -216,6 +216,7 @@ onUnmounted(() => {
&.active { &.active {
opacity: 1; opacity: 1;
color: var(--accent);
} }
&.animate { &.animate {

View file

@ -48,7 +48,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</MkInput> </MkInput>
<FromSlot> <FromSlot>
<template #label>Default like emoji</template> <template #label>{{ i18n.ts.defaultLike }}</template>
<MkCustomEmoji v-if="defaultLike.startsWith(':')" style="max-height: 3em; font-size: 1.1em;" :useOriginalSize="false" :class="$style.reaction" :name="defaultLike" :normal="true" :noStyle="true"/> <MkCustomEmoji v-if="defaultLike.startsWith(':')" style="max-height: 3em; font-size: 1.1em;" :useOriginalSize="false" :class="$style.reaction" :name="defaultLike" :normal="true" :noStyle="true"/>
<MkEmoji v-else :emoji="defaultLike" style="max-height: 3em; font-size: 1.1em;" :normal="true" :noStyle="true"/> <MkEmoji v-else :emoji="defaultLike" style="max-height: 3em; font-size: 1.1em;" :normal="true" :noStyle="true"/>
<MkButton rounded :small="true" @click="chooseNewLike"><i class="ph-smiley ph-bold ph-lg"></i> Change</MkButton> <MkButton rounded :small="true" @click="chooseNewLike"><i class="ph-smiley ph-bold ph-lg"></i> Change</MkButton>

View file

@ -166,7 +166,6 @@ SPDX-License-Identifier: AGPL-3.0-only
</div> </div>
<MkSelect v-model="serverDisconnectedBehavior"> <MkSelect v-model="serverDisconnectedBehavior">
<template #label>{{ i18n.ts.whenServerDisconnected }}</template> <template #label>{{ i18n.ts.whenServerDisconnected }}</template>
<option value="reload">{{ i18n.ts._serverDisconnectedBehavior.reload }}</option>
<option value="dialog">{{ i18n.ts._serverDisconnectedBehavior.dialog }}</option> <option value="dialog">{{ i18n.ts._serverDisconnectedBehavior.dialog }}</option>
<option value="quiet">{{ i18n.ts._serverDisconnectedBehavior.quiet }}</option> <option value="quiet">{{ i18n.ts._serverDisconnectedBehavior.quiet }}</option>
<option value="disabled">{{ i18n.ts._serverDisconnectedBehavior.disabled }}</option> <option value="disabled">{{ i18n.ts._serverDisconnectedBehavior.disabled }}</option>

View file

@ -24,9 +24,10 @@ SPDX-License-Identifier: AGPL-3.0-only
</FromSlot> </FromSlot>
<FromSlot> <FromSlot>
<template #label>Default like emoji</template> <template #label>{{ i18n.ts.defaultLike }}</template>
<MkCustomEmoji v-if="like.startsWith(':')" style="max-height: 3em; font-size: 1.1em;" :useOriginalSize="false" :class="$style.reaction" :name="like" :normal="true" :noStyle="true"/> <MkCustomEmoji v-if="like && like.startsWith(':')" style="max-height: 3em; font-size: 1.1em;" :useOriginalSize="false" :class="$style.reaction" :name="like" :normal="true" :noStyle="true"/>
<MkEmoji v-else :emoji="like" style="max-height: 3em; font-size: 1.1em;" :normal="true" :noStyle="true"/> <MkEmoji v-else-if="like && !like.startsWith(':')" :emoji="like" style="max-height: 3em; font-size: 1.1em;" :normal="true" :noStyle="true"/>
<span v-else-if="!like">{{ i18n.ts.notSet }}</span>
<div class="_buttons" style="padding-top: 8px;"> <div class="_buttons" style="padding-top: 8px;">
<MkButton rounded :small="true" inline @click="chooseNewLike"><i class="ph-smiley ph-bold ph-lg"></i> Change</MkButton> <MkButton rounded :small="true" inline @click="chooseNewLike"><i class="ph-smiley ph-bold ph-lg"></i> Change</MkButton>
<MkButton rounded :small="true" inline @click="resetLike"><i class="ph-arrow-clockwise ph-bold ph-lg"></i> Reset</MkButton> <MkButton rounded :small="true" inline @click="resetLike"><i class="ph-arrow-clockwise ph-bold ph-lg"></i> Reset</MkButton>
@ -82,6 +83,7 @@ import { defaultStore } from '@/store.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 { deepClone } from '@/scripts/clone.js'; import { deepClone } from '@/scripts/clone.js';
import { unisonReload } from '@/scripts/unison-reload.js';
let reactions = $ref(deepClone(defaultStore.state.reactions)); let reactions = $ref(deepClone(defaultStore.state.reactions));
const like = $computed(defaultStore.makeGetterSetter('like')); const like = $computed(defaultStore.makeGetterSetter('like'));
@ -91,6 +93,16 @@ const reactionPickerWidth = $computed(defaultStore.makeGetterSetter('reactionPic
const reactionPickerHeight = $computed(defaultStore.makeGetterSetter('reactionPickerHeight')); const reactionPickerHeight = $computed(defaultStore.makeGetterSetter('reactionPickerHeight'));
const reactionPickerUseDrawerForMobile = $computed(defaultStore.makeGetterSetter('reactionPickerUseDrawerForMobile')); const reactionPickerUseDrawerForMobile = $computed(defaultStore.makeGetterSetter('reactionPickerUseDrawerForMobile'));
async function reloadAsk() {
const { canceled } = await os.confirm({
type: 'info',
text: i18n.ts.reloadToApplySetting,
});
if (canceled) return;
unisonReload();
}
function save() { function save() {
defaultStore.set('reactions', reactions); defaultStore.set('reactions', reactions);
} }
@ -134,13 +146,15 @@ function chooseEmoji(ev: MouseEvent) {
function chooseNewLike(ev: MouseEvent) { function chooseNewLike(ev: MouseEvent) {
os.pickEmoji(ev.currentTarget ?? ev.target, { os.pickEmoji(ev.currentTarget ?? ev.target, {
showPinned: false, showPinned: false,
}).then(emoji => { }).then(async emoji => {
defaultStore.set('like', emoji as string); defaultStore.set('like', emoji as string);
await reloadAsk();
}); });
} }
function resetLike() { async function resetLike() {
defaultStore.set('like', '❤️'); defaultStore.set('like', null);
await reloadAsk();
} }
watch($$(reactions), () => { watch($$(reactions), () => {

View file

@ -176,7 +176,13 @@ export function getUserMenu(user: Misskey.entities.UserDetailed, router: Router
const canonical = user.host === null ? `@${user.username}` : `@${user.username}@${toUnicode(user.host)}`; const canonical = user.host === null ? `@${user.username}` : `@${user.username}@${toUnicode(user.host)}`;
copyToClipboard(`${url}/${canonical}`); copyToClipboard(`${url}/${canonical}`);
}, },
}, { }, ...(user.host ? [{
icon: 'ph-share ph-bold ph-lg',
text: i18n.ts.openRemoteProfile,
action: () => {
open(`${user.uri}`, '_blank');
},
}] : []), {
icon: 'ph-envelope ph-bold ph-lg', icon: 'ph-envelope ph-bold ph-lg',
text: i18n.ts.sendMessage, text: i18n.ts.sendMessage,
action: () => { action: () => {

View file

@ -112,7 +112,7 @@ export const defaultStore = markRaw(new Storage('base', {
}, },
like: { like: {
where: 'account', where: 'account',
default: '❤️', default: null as string | null,
}, },
mutedAds: { mutedAds: {
where: 'account', where: 'account',
@ -188,7 +188,7 @@ export const defaultStore = markRaw(new Storage('base', {
}, },
serverDisconnectedBehavior: { serverDisconnectedBehavior: {
where: 'device', where: 'device',
default: 'quiet' as 'quiet' | 'reload' | 'dialog' | 'disabled', default: 'disabled' as 'quiet' | 'dialog' | 'disabled',
}, },
nsfw: { nsfw: {
where: 'device', where: 'device',

View file

@ -109,7 +109,8 @@ watch(defaultStore.reactiveState.menuDisplay, () => {
$nav-icon-only-width: 78px; // TODO: $nav-icon-only-width: 78px; // TODO:
$avatar-size: 32px; $avatar-size: 32px;
$avatar-margin: 8px; $avatar-margin: 8px;
position: sticky;
top: 16px;
padding: 0 16px; padding: 0 16px;
box-sizing: border-box; box-sizing: border-box;
width: 260px; width: 260px;

View file

@ -253,9 +253,13 @@ onMounted(() => {
} }
> .widgets { > .widgets {
//--panelBorder: none; position: sticky;
top: 0;
width: 300px; width: 300px;
padding-bottom: calc(var(--margin) + env(safe-area-inset-bottom, 0px)); height: 100%;
padding-top: 16px;
box-sizing: border-box;
overflow: auto;
@media (max-width: $widgets-hide-threshold) { @media (max-width: $widgets-hide-threshold) {
display: none; display: none;

View file

@ -324,7 +324,7 @@ $widgets-hide-threshold: 1090px;
min-width: 0; min-width: 0;
overflow: auto; overflow: auto;
overflow-y: scroll; overflow-y: scroll;
overscroll-behavior: contain; overscroll-behavior: unset;
background: var(--bg); background: var(--bg);
} }

View file

@ -0,0 +1,155 @@
<!--
SPDX-FileCopyrightText: syuilo and other misskey contributors
SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<MkContainer :showHeader="widgetProps.showHeader" class="skw-search">
<MkInput v-model="searchQuery" :large="true" :autofocus="true" type="search" @keydown="onInputKeydown">
<template #suffix>
<button style="border: none; background: none; margin-right: 0.5em; z-index: 2; pointer-events: auto; position: relative; margin-top: 0 auto;" @click="options"><i class="ph-funnel ph-bold ph-lg"></i></button>
<button style="border: none; background: none; z-index: 2; pointer-events: auto; position: relative; margin: 0 auto;" @click="search"><i class="ph-magnifying-glass ph-bold ph-lg"></i></button>
</template>
</MkInput>
</MkContainer>
</template>
<script lang="ts" setup>
import { computed, defineAsyncComponent, onMounted } from 'vue';
import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js';
import MkInput from '@/components/MkInput.vue';
import MkContainer from '@/components/MkContainer.vue';
import { i18n } from '@/i18n.js';
import * as os from '@/os.js';
import { useRouter } from '@/router.js';
import { GetFormResultType } from '@/scripts/form.js';
const name = 'search';
const widgetPropsDef = {
showHeader: {
type: 'boolean' as const,
default: false,
},
};
type WidgetProps = GetFormResultType<typeof widgetPropsDef>;
const props = defineProps<WidgetComponentProps<WidgetProps>>();
const emit = defineEmits<WidgetComponentEmits<WidgetProps>>();
const { widgetProps, configure } = useWidgetPropsManager(name,
widgetPropsDef,
props,
emit,
);
function onInputKeydown(evt: KeyboardEvent) {
if (evt.key === 'Enter') {
evt.preventDefault();
evt.stopPropagation();
search();
}
}
const router = useRouter();
let key = $ref(0);
let searchQuery = $ref('');
let notePagination = $ref();
let searchOrigin = $ref('combined');
let user = $ref(null);
let isLocalOnly = $ref(false);
let order = $ref(true);
let filetype = $ref(null);
function options(ev) {
os.popupMenu([{
type: 'parent',
text: 'With File',
icon: 'ph-file ph-bold ph-lg',
children: [
{
type: 'button',
icon: 'ph-image ph-bold ph-lg',
text: 'With Images',
action: () => {
filetype = 'image';
},
},
{
type: 'button',
icon: 'ph-music-notes-simple ph-bold ph-lg',
text: 'With Audios',
action: () => {
filetype = 'audio';
},
},
{
type: 'button',
icon: 'ph-video ph-bold ph-lg',
text: 'With Videos',
action: () => {
filetype = 'video';
},
}],
}], ev.currentTarget ?? ev.target);
}
function selectUser() {
os.selectUser().then(_user => {
user = _user;
});
}
async function search() {
const query = searchQuery.toString().trim();
if (query == null || query === '') return;
if (query.startsWith('https://')) {
const promise = os.api('ap/show', {
uri: query,
});
os.promiseDialog(promise, null, null, i18n.ts.fetchingAsApObject);
const res = await promise;
if (res.type === 'User') {
router.push(`/@${res.object.username}@${res.object.host}`);
} else if (res.type === 'Note') {
router.push(`/notes/${res.object.id}`);
}
return;
}
notePagination = {
endpoint: 'notes/search',
limit: 10,
params: {
query: searchQuery,
userId: user ? user.id : null,
order: order ? 'desc' : 'asc',
filetype: filetype,
},
};
if (isLocalOnly) notePagination.params.host = '.';
key++;
os.popup(defineAsyncComponent(() => import('@/components/SkSearchResultWindow.vue')), {
noteKey: key,
notePagination: notePagination,
}, {
}, 'closed');
}
defineExpose<WidgetComponentExpose>({
name,
configure,
id: props.widget ? props.widget.id : null,
});
</script>

View file

@ -33,6 +33,7 @@ export default function(app: App) {
app.component('WidgetAichan', defineAsyncComponent(() => import('./WidgetAichan.vue'))); app.component('WidgetAichan', defineAsyncComponent(() => import('./WidgetAichan.vue')));
app.component('WidgetUserList', defineAsyncComponent(() => import('./WidgetUserList.vue'))); app.component('WidgetUserList', defineAsyncComponent(() => import('./WidgetUserList.vue')));
app.component('WidgetClicker', defineAsyncComponent(() => import('./WidgetClicker.vue'))); app.component('WidgetClicker', defineAsyncComponent(() => import('./WidgetClicker.vue')));
app.component('WidgetSearch', defineAsyncComponent(() => import('./WidgetSearch.vue')));
} }
export const widgets = [ export const widgets = [
@ -63,4 +64,5 @@ export const widgets = [
'aichan', 'aichan',
'userList', 'userList',
'clicker', 'clicker',
'search',
]; ];

View file

@ -63,7 +63,7 @@
"@types/parse-link-header": "^2.0.3", "@types/parse-link-header": "^2.0.3",
"@types/uuid": "^9.0.7", "@types/uuid": "^9.0.7",
"@types/ws": "^8.5.10", "@types/ws": "^8.5.10",
"axios": "1.5.0", "axios": "1.6.0",
"dayjs": "^1.11.10", "dayjs": "^1.11.10",
"form-data": "^4.0.0", "form-data": "^4.0.0",
"https-proxy-agent": "^7.0.2", "https-proxy-agent": "^7.0.2",

View file

@ -217,6 +217,7 @@ export type Note = {
clippedCount?: number; clippedCount?: number;
poll?: { poll?: {
expiresAt: DateString | null; expiresAt: DateString | null;
expiredAfter: DateString | null;
multiple: boolean; multiple: boolean;
choices: { choices: {
isVoted: boolean; isVoted: boolean;

View file

@ -1034,8 +1034,8 @@ importers:
specifier: ^8.5.10 specifier: ^8.5.10
version: 8.5.10 version: 8.5.10
axios: axios:
specifier: 1.5.0 specifier: 1.6.0
version: 1.5.0 version: 1.6.0
dayjs: dayjs:
specifier: ^1.11.10 specifier: ^1.11.10
version: 1.11.10 version: 1.11.10
@ -2285,15 +2285,6 @@ packages:
'@babel/helper-plugin-utils': 7.22.5 '@babel/helper-plugin-utils': 7.22.5
dev: true dev: true
/@babel/plugin-syntax-bigint@7.8.3(@babel/core@7.22.11):
resolution: {integrity: sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==}
peerDependencies:
'@babel/core': ^7.0.0-0
dependencies:
'@babel/core': 7.22.11
'@babel/helper-plugin-utils': 7.22.5
dev: true
/@babel/plugin-syntax-bigint@7.8.3(@babel/core@7.23.3): /@babel/plugin-syntax-bigint@7.8.3(@babel/core@7.23.3):
resolution: {integrity: sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==} resolution: {integrity: sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==}
peerDependencies: peerDependencies:
@ -7294,7 +7285,7 @@ packages:
ts-dedent: 2.2.0 ts-dedent: 2.2.0
type-fest: 2.19.0 type-fest: 2.19.0
vue: 3.3.8(typescript@5.2.2) vue: 3.3.8(typescript@5.2.2)
vue-component-type-helpers: 1.8.22 vue-component-type-helpers: 1.8.24
transitivePeerDependencies: transitivePeerDependencies:
- encoding - encoding
- supports-color - supports-color
@ -7940,10 +7931,6 @@ packages:
'@types/unist': 2.0.6 '@types/unist': 2.0.6
dev: true dev: true
/@types/http-cache-semantics@4.0.1:
resolution: {integrity: sha512-SZs7ekbP8CN0txVG2xVRH6EgKmEm31BOxA07vkFaETzZz1xh+cbt8BcI0slpymvwhx5dlFnQG2rTlPVQn+iRPQ==}
dev: false
/@types/http-cache-semantics@4.0.4: /@types/http-cache-semantics@4.0.4:
resolution: {integrity: sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA==} resolution: {integrity: sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA==}
@ -9198,7 +9185,7 @@ packages:
requiresBuild: true requiresBuild: true
dependencies: dependencies:
delegates: 1.0.0 delegates: 1.0.0
readable-stream: 3.6.0 readable-stream: 3.6.2
dev: false dev: false
/arg@5.0.2: /arg@5.0.2:
@ -9470,8 +9457,8 @@ packages:
- debug - debug
dev: false dev: false
/axios@1.5.0: /axios@1.6.0:
resolution: {integrity: sha512-D4DdjDo5CY50Qms0qGQTTw6Q44jl7zRwY7bthds06pUGfChBCTcQs+N743eFWGEd6pRTMd6A+I87aWyFV5wiZQ==} resolution: {integrity: sha512-EZ1DYihju9pwVB+jg67ogm+Tmqc6JmhamRN6I4Zt8DfZu5lbcQGw3ozH9lFejSJgs/ibaef3A9PMXPLeefFGJg==}
dependencies: dependencies:
follow-redirects: 1.15.3(debug@4.3.4) follow-redirects: 1.15.3(debug@4.3.4)
form-data: 4.0.0 form-data: 4.0.0
@ -9501,24 +9488,6 @@ packages:
'@babel/core': 7.22.11 '@babel/core': 7.22.11
dev: true dev: true
/babel-jest@29.7.0(@babel/core@7.22.11):
resolution: {integrity: sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==}
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
peerDependencies:
'@babel/core': ^7.8.0
dependencies:
'@babel/core': 7.22.11
'@jest/transform': 29.7.0
'@types/babel__core': 7.20.0
babel-plugin-istanbul: 6.1.1
babel-preset-jest: 29.6.3(@babel/core@7.22.11)
chalk: 4.1.2
graceful-fs: 4.2.11
slash: 3.0.0
transitivePeerDependencies:
- supports-color
dev: true
/babel-jest@29.7.0(@babel/core@7.23.3): /babel-jest@29.7.0(@babel/core@7.23.3):
resolution: {integrity: sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==} resolution: {integrity: sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==}
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
@ -9596,26 +9565,6 @@ packages:
- supports-color - supports-color
dev: true dev: true
/babel-preset-current-node-syntax@1.0.1(@babel/core@7.22.11):
resolution: {integrity: sha512-M7LQ0bxarkxQoN+vz5aJPsLBn77n8QgTFmo8WK0/44auK2xlCXrYcUxHFxgU7qW5Yzw/CjmLRK2uJzaCd7LvqQ==}
peerDependencies:
'@babel/core': ^7.0.0
dependencies:
'@babel/core': 7.22.11
'@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.22.11)
'@babel/plugin-syntax-bigint': 7.8.3(@babel/core@7.22.11)
'@babel/plugin-syntax-class-properties': 7.12.13(@babel/core@7.22.11)
'@babel/plugin-syntax-import-meta': 7.10.4(@babel/core@7.22.11)
'@babel/plugin-syntax-json-strings': 7.8.3(@babel/core@7.22.11)
'@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@7.22.11)
'@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.22.11)
'@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@7.22.11)
'@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.22.11)
'@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.22.11)
'@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.22.11)
'@babel/plugin-syntax-top-level-await': 7.14.5(@babel/core@7.22.11)
dev: true
/babel-preset-current-node-syntax@1.0.1(@babel/core@7.23.3): /babel-preset-current-node-syntax@1.0.1(@babel/core@7.23.3):
resolution: {integrity: sha512-M7LQ0bxarkxQoN+vz5aJPsLBn77n8QgTFmo8WK0/44auK2xlCXrYcUxHFxgU7qW5Yzw/CjmLRK2uJzaCd7LvqQ==} resolution: {integrity: sha512-M7LQ0bxarkxQoN+vz5aJPsLBn77n8QgTFmo8WK0/44auK2xlCXrYcUxHFxgU7qW5Yzw/CjmLRK2uJzaCd7LvqQ==}
peerDependencies: peerDependencies:
@ -9636,17 +9585,6 @@ packages:
'@babel/plugin-syntax-top-level-await': 7.14.5(@babel/core@7.23.3) '@babel/plugin-syntax-top-level-await': 7.14.5(@babel/core@7.23.3)
dev: true dev: true
/babel-preset-jest@29.6.3(@babel/core@7.22.11):
resolution: {integrity: sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==}
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
peerDependencies:
'@babel/core': ^7.0.0
dependencies:
'@babel/core': 7.22.11
babel-plugin-jest-hoist: 29.6.3
babel-preset-current-node-syntax: 1.0.1(@babel/core@7.22.11)
dev: true
/babel-preset-jest@29.6.3(@babel/core@7.23.3): /babel-preset-jest@29.6.3(@babel/core@7.23.3):
resolution: {integrity: sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==} resolution: {integrity: sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==}
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
@ -9725,7 +9663,7 @@ packages:
dependencies: dependencies:
buffer: 5.7.1 buffer: 5.7.1
inherits: 2.0.4 inherits: 2.0.4
readable-stream: 3.6.0 readable-stream: 3.6.2
/blob-util@2.0.2: /blob-util@2.0.2:
resolution: {integrity: sha512-T7JQa+zsXXEa6/8ZhHcQEW1UFfVM49Ts65uBkFL6fz2QmrElqmbajIDJvuA0tEhRe5eIjpV9ZF+0RfZR9voJFQ==} resolution: {integrity: sha512-T7JQa+zsXXEa6/8ZhHcQEW1UFfVM49Ts65uBkFL6fz2QmrElqmbajIDJvuA0tEhRe5eIjpV9ZF+0RfZR9voJFQ==}
@ -9992,10 +9930,10 @@ packages:
resolution: {integrity: sha512-IDVO5MJ4LItE6HKFQTqT2ocAQsisOoCTUDu1ddCmnhyiwFQjXNPp4081Xj23N4tO+AFEFNzGuNEf/c8Gwwt15A==} resolution: {integrity: sha512-IDVO5MJ4LItE6HKFQTqT2ocAQsisOoCTUDu1ddCmnhyiwFQjXNPp4081Xj23N4tO+AFEFNzGuNEf/c8Gwwt15A==}
engines: {node: '>=14.16'} engines: {node: '>=14.16'}
dependencies: dependencies:
'@types/http-cache-semantics': 4.0.1 '@types/http-cache-semantics': 4.0.4
get-stream: 6.0.1 get-stream: 6.0.1
http-cache-semantics: 4.1.1 http-cache-semantics: 4.1.1
keyv: 4.5.2 keyv: 4.5.4
mimic-response: 4.0.0 mimic-response: 4.0.0
normalize-url: 8.0.0 normalize-url: 8.0.0
responselike: 3.0.0 responselike: 3.0.0
@ -10504,7 +10442,7 @@ packages:
crc-32: 1.2.2 crc-32: 1.2.2
crc32-stream: 5.0.0 crc32-stream: 5.0.0
normalize-path: 3.0.0 normalize-path: 3.0.0
readable-stream: 3.6.0 readable-stream: 3.6.2
dev: false dev: false
/compressible@2.0.18: /compressible@2.0.18:
@ -10635,28 +10573,9 @@ packages:
engines: {node: '>= 12.0.0'} engines: {node: '>= 12.0.0'}
dependencies: dependencies:
crc-32: 1.2.2 crc-32: 1.2.2
readable-stream: 3.6.0 readable-stream: 3.6.2
dev: false dev: false
/create-jest@29.7.0:
resolution: {integrity: sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==}
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
hasBin: true
dependencies:
'@jest/types': 29.6.3
chalk: 4.1.2
exit: 0.1.2
graceful-fs: 4.2.11
jest-config: 29.7.0(@types/node@20.9.4)
jest-util: 29.7.0
prompts: 2.4.2
transitivePeerDependencies:
- '@types/node'
- babel-plugin-macros
- supports-color
- ts-node
dev: true
/create-jest@29.7.0(@types/node@20.9.1): /create-jest@29.7.0(@types/node@20.9.1):
resolution: {integrity: sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==} resolution: {integrity: sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==}
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
@ -11091,6 +11010,7 @@ packages:
/deepmerge@4.2.2: /deepmerge@4.2.2:
resolution: {integrity: sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg==} resolution: {integrity: sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
dev: false
/deepmerge@4.3.1: /deepmerge@4.3.1:
resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==}
@ -11314,7 +11234,7 @@ packages:
dependencies: dependencies:
end-of-stream: 1.4.4 end-of-stream: 1.4.4
inherits: 2.0.4 inherits: 2.0.4
readable-stream: 2.3.7 readable-stream: 2.3.8
stream-shift: 1.0.1 stream-shift: 1.0.1
dev: true dev: true
@ -13951,10 +13871,10 @@ packages:
'@jest/test-result': 29.7.0 '@jest/test-result': 29.7.0
'@jest/types': 29.6.3 '@jest/types': 29.6.3
chalk: 4.1.2 chalk: 4.1.2
create-jest: 29.7.0 create-jest: 29.7.0(@types/node@20.9.1)
exit: 0.1.2 exit: 0.1.2
import-local: 3.1.0 import-local: 3.1.0
jest-config: 29.7.0(@types/node@20.9.4) jest-config: 29.7.0(@types/node@20.9.1)
jest-util: 29.7.0 jest-util: 29.7.0
jest-validate: 29.7.0 jest-validate: 29.7.0
yargs: 17.7.2 yargs: 17.7.2
@ -14005,14 +13925,14 @@ packages:
ts-node: ts-node:
optional: true optional: true
dependencies: dependencies:
'@babel/core': 7.22.11 '@babel/core': 7.23.3
'@jest/test-sequencer': 29.7.0 '@jest/test-sequencer': 29.7.0
'@jest/types': 29.6.3 '@jest/types': 29.6.3
'@types/node': 20.9.1 '@types/node': 20.9.1
babel-jest: 29.7.0(@babel/core@7.22.11) babel-jest: 29.7.0(@babel/core@7.23.3)
chalk: 4.1.2 chalk: 4.1.2
ci-info: 3.7.1 ci-info: 3.9.0
deepmerge: 4.2.2 deepmerge: 4.3.1
glob: 7.2.3 glob: 7.2.3
graceful-fs: 4.2.11 graceful-fs: 4.2.11
jest-circus: 29.7.0 jest-circus: 29.7.0
@ -14741,12 +14661,6 @@ packages:
safe-buffer: 5.2.1 safe-buffer: 5.2.1
dev: false dev: false
/keyv@4.5.2:
resolution: {integrity: sha512-5MHbFaKn8cNSmVW7BYnijeAVlE4cYA/SVkifVgrh7yotnfhKmjuXpDKjrABLnT0SfHWV21P8ow07OGfRrNDg8g==}
dependencies:
json-buffer: 3.0.1
dev: false
/keyv@4.5.4: /keyv@4.5.4:
resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==}
dependencies: dependencies:
@ -14800,7 +14714,7 @@ packages:
resolution: {integrity: sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==} resolution: {integrity: sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==}
engines: {node: '>= 0.6.3'} engines: {node: '>= 0.6.3'}
dependencies: dependencies:
readable-stream: 2.3.7 readable-stream: 2.3.8
dev: false dev: false
/leven@3.1.0: /leven@3.1.0:
@ -17504,17 +17418,6 @@ packages:
type-fest: 0.6.0 type-fest: 0.6.0
dev: true dev: true
/readable-stream@2.3.7:
resolution: {integrity: sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==}
dependencies:
core-util-is: 1.0.3
inherits: 2.0.4
isarray: 1.0.0
process-nextick-args: 2.0.1
safe-buffer: 5.1.2
string_decoder: 1.1.1
util-deprecate: 1.0.2
/readable-stream@2.3.8: /readable-stream@2.3.8:
resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==} resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==}
dependencies: dependencies:
@ -17525,7 +17428,6 @@ packages:
safe-buffer: 5.1.2 safe-buffer: 5.1.2
string_decoder: 1.1.1 string_decoder: 1.1.1
util-deprecate: 1.0.2 util-deprecate: 1.0.2
dev: true
/readable-stream@3.6.0: /readable-stream@3.6.0:
resolution: {integrity: sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==} resolution: {integrity: sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==}
@ -17534,6 +17436,7 @@ packages:
inherits: 2.0.4 inherits: 2.0.4
string_decoder: 1.3.0 string_decoder: 1.3.0
util-deprecate: 1.0.2 util-deprecate: 1.0.2
dev: false
/readable-stream@3.6.2: /readable-stream@3.6.2:
resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==}
@ -18995,7 +18898,7 @@ packages:
/through2@2.0.5: /through2@2.0.5:
resolution: {integrity: sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==} resolution: {integrity: sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==}
dependencies: dependencies:
readable-stream: 2.3.7 readable-stream: 2.3.8
xtend: 4.0.2 xtend: 4.0.2
dev: true dev: true
@ -19956,8 +19859,8 @@ packages:
/vscode-textmate@8.0.0: /vscode-textmate@8.0.0:
resolution: {integrity: sha512-AFbieoL7a5LMqcnOF04ji+rpXadgOXnZsxQr//r83kLPr7biP7am3g9zbaZIaBGwBRWeSvoMD4mgPdX3e4NWBg==} resolution: {integrity: sha512-AFbieoL7a5LMqcnOF04ji+rpXadgOXnZsxQr//r83kLPr7biP7am3g9zbaZIaBGwBRWeSvoMD4mgPdX3e4NWBg==}
/vue-component-type-helpers@1.8.22: /vue-component-type-helpers@1.8.24:
resolution: {integrity: sha512-LK3wJHs3vJxHG292C8cnsRusgyC5SEZDCzDCD01mdE/AoREFMl2tzLRuzwyuEsOIz13tqgBcnvysN3Lxsa14Fw==} resolution: {integrity: sha512-lqWs/7fdRXoSBAlbouHBX+LNuaY6gI9xWW34m/ZIz9zVPYHEyw0b2/zaCBwlKx0NtKTeF/6pOpvrxVkh7nhIYg==}
dev: true dev: true
/vue-component-type-helpers@1.8.4: /vue-component-type-helpers@1.8.4: