summaryrefslogtreecommitdiff
path: root/packages/frontend/src
diff options
context:
space:
mode:
authorMarie <Marie@kaifa.ch>2023-12-23 02:09:23 +0100
committerMarie <Marie@kaifa.ch>2023-12-23 02:09:23 +0100
commit5db583a3eb61d50de14d875ebf7ecef20490e313 (patch)
tree783dd43d2ac660c32e745a4485d499e9ddc43324 /packages/frontend/src
parentadd: Custom MOTDs (diff)
parentUpdate CHANGELOG.md (diff)
downloadsharkey-5db583a3eb61d50de14d875ebf7ecef20490e313.tar.gz
sharkey-5db583a3eb61d50de14d875ebf7ecef20490e313.tar.bz2
sharkey-5db583a3eb61d50de14d875ebf7ecef20490e313.zip
merge: upstream
Diffstat (limited to 'packages/frontend/src')
-rw-r--r--packages/frontend/src/_dev_boot_.ts95
-rw-r--r--packages/frontend/src/account.ts4
-rw-r--r--packages/frontend/src/boot/common.ts47
-rw-r--r--packages/frontend/src/boot/main-boot.ts18
-rw-r--r--packages/frontend/src/boot/sub-boot.ts2
-rw-r--r--packages/frontend/src/components/MkAbuseReport.vue7
-rw-r--r--packages/frontend/src/components/MkAchievements.vue10
-rw-r--r--packages/frontend/src/components/MkAnalogClock.vue64
-rw-r--r--packages/frontend/src/components/MkAnimBg.vue32
-rw-r--r--packages/frontend/src/components/MkAsUi.vue8
-rw-r--r--packages/frontend/src/components/MkAutocomplete.vue111
-rw-r--r--packages/frontend/src/components/MkButton.vue14
-rw-r--r--packages/frontend/src/components/MkChannelPreview.vue98
-rw-r--r--packages/frontend/src/components/MkChart.vue4
-rw-r--r--packages/frontend/src/components/MkChartLegend.vue19
-rw-r--r--packages/frontend/src/components/MkClickerGame.vue14
-rw-r--r--packages/frontend/src/components/MkCode.core.vue4
-rw-r--r--packages/frontend/src/components/MkCode.vue65
-rw-r--r--packages/frontend/src/components/MkCodeEditor.vue92
-rw-r--r--packages/frontend/src/components/MkColorInput.vue3
-rw-r--r--packages/frontend/src/components/MkContextMenu.vue16
-rw-r--r--packages/frontend/src/components/MkCropperDialog.vue16
-rw-r--r--packages/frontend/src/components/MkCwButton.vue25
-rw-r--r--packages/frontend/src/components/MkDialog.vue19
-rw-r--r--packages/frontend/src/components/MkDrive.folder.vue9
-rw-r--r--packages/frontend/src/components/MkDrive.vue4
-rw-r--r--packages/frontend/src/components/MkEmojiPicker.section.vue47
-rw-r--r--packages/frontend/src/components/MkEmojiPicker.vue83
-rw-r--r--packages/frontend/src/components/MkEmojiPickerDialog.vue13
-rw-r--r--packages/frontend/src/components/MkFeaturedPhotos.vue2
-rw-r--r--packages/frontend/src/components/MkFileCaptionEditWindow.vue10
-rw-r--r--packages/frontend/src/components/MkFolder.vue20
-rw-r--r--packages/frontend/src/components/MkFollowButton.vue26
-rw-r--r--packages/frontend/src/components/MkForgotPassword.vue18
-rw-r--r--packages/frontend/src/components/MkFormDialog.vue4
-rw-r--r--packages/frontend/src/components/MkGoogle.vue2
-rw-r--r--packages/frontend/src/components/MkHeatmap.vue18
-rw-r--r--packages/frontend/src/components/MkImgWithBlurhash.vue45
-rw-r--r--packages/frontend/src/components/MkInput.vue15
-rw-r--r--packages/frontend/src/components/MkInstanceCardMini.vue9
-rw-r--r--packages/frontend/src/components/MkInstanceStats.vue16
-rw-r--r--packages/frontend/src/components/MkInstanceTicker.vue4
-rw-r--r--packages/frontend/src/components/MkInviteCode.vue2
-rw-r--r--packages/frontend/src/components/MkLaunchPad.vue8
-rw-r--r--packages/frontend/src/components/MkLink.vue10
-rw-r--r--packages/frontend/src/components/MkMediaBanner.vue4
-rw-r--r--packages/frontend/src/components/MkMediaImage.vue22
-rw-r--r--packages/frontend/src/components/MkMediaList.vue81
-rw-r--r--packages/frontend/src/components/MkMediaVideo.vue13
-rw-r--r--packages/frontend/src/components/MkMenu.vue98
-rw-r--r--packages/frontend/src/components/MkMiniChart.vue20
-rw-r--r--packages/frontend/src/components/MkModPlayer.vue10
-rw-r--r--packages/frontend/src/components/MkModal.vue92
-rw-r--r--packages/frontend/src/components/MkModalWindow.vue24
-rw-r--r--packages/frontend/src/components/MkNote.vue195
-rw-r--r--packages/frontend/src/components/MkNoteDetailed.vue145
-rw-r--r--packages/frontend/src/components/MkNotePreview.vue28
-rw-r--r--packages/frontend/src/components/MkNoteSimple.vue9
-rw-r--r--packages/frontend/src/components/MkNoteSub.vue43
-rw-r--r--packages/frontend/src/components/MkNotes.vue2
-rw-r--r--packages/frontend/src/components/MkNotification.vue8
-rw-r--r--packages/frontend/src/components/MkNotificationSelectWindow.vue6
-rw-r--r--packages/frontend/src/components/MkNotifications.vue11
-rw-r--r--packages/frontend/src/components/MkOmit.vue14
-rw-r--r--packages/frontend/src/components/MkPagePreview.vue2
-rw-r--r--packages/frontend/src/components/MkPageWindow.vue40
-rw-r--r--packages/frontend/src/components/MkPagination.vue63
-rw-r--r--packages/frontend/src/components/MkPasswordDialog.vue18
-rw-r--r--packages/frontend/src/components/MkPlusOneEffect.vue6
-rw-r--r--packages/frontend/src/components/MkPopupMenu.vue10
-rw-r--r--packages/frontend/src/components/MkPostForm.vue407
-rw-r--r--packages/frontend/src/components/MkPostFormDialog.vue9
-rw-r--r--packages/frontend/src/components/MkPullToRefresh.vue79
-rw-r--r--packages/frontend/src/components/MkPushNotificationAllowButton.vue41
-rw-r--r--packages/frontend/src/components/MkRadio.vue4
-rw-r--r--packages/frontend/src/components/MkReactionEffect.vue6
-rw-r--r--packages/frontend/src/components/MkReactionsViewer.reaction.vue17
-rw-r--r--packages/frontend/src/components/MkReactionsViewer.vue26
-rw-r--r--packages/frontend/src/components/MkRetentionHeatmap.vue16
-rw-r--r--packages/frontend/src/components/MkSignin.vue90
-rw-r--r--packages/frontend/src/components/MkSigninDialog.vue8
-rw-r--r--packages/frontend/src/components/MkSignupDialog.form.vue152
-rw-r--r--packages/frontend/src/components/MkSignupDialog.rules.vue4
-rw-r--r--packages/frontend/src/components/MkSignupDialog.vue13
-rw-r--r--packages/frontend/src/components/MkSubNoteContent.vue25
-rw-r--r--packages/frontend/src/components/MkSwitch.vue5
-rw-r--r--packages/frontend/src/components/MkTagCloud.vue20
-rw-r--r--packages/frontend/src/components/MkTextarea.vue41
-rw-r--r--packages/frontend/src/components/MkTimeline.vue10
-rw-r--r--packages/frontend/src/components/MkToast.vue6
-rw-r--r--packages/frontend/src/components/MkTokenGenerateWindow.vue26
-rw-r--r--packages/frontend/src/components/MkTutorialDialog.Timeline.vue2
-rw-r--r--packages/frontend/src/components/MkTutorialDialog.vue2
-rw-r--r--packages/frontend/src/components/MkUrlPreview.vue62
-rw-r--r--packages/frontend/src/components/MkUrlPreviewPopup.vue10
-rw-r--r--packages/frontend/src/components/MkUserAnnouncementEditDialog.vue32
-rw-r--r--packages/frontend/src/components/MkUserCardMini.vue6
-rw-r--r--packages/frontend/src/components/MkUserInfo.vue6
-rw-r--r--packages/frontend/src/components/MkUserOnlineIndicator.vue4
-rw-r--r--packages/frontend/src/components/MkUserPopup.vue24
-rw-r--r--packages/frontend/src/components/MkUserSelectDialog.vue40
-rw-r--r--packages/frontend/src/components/MkUserSetupDialog.Follow.vue6
-rw-r--r--packages/frontend/src/components/MkUserSetupDialog.Privacy.vue10
-rw-r--r--packages/frontend/src/components/MkUserSetupDialog.Profile.vue3
-rw-r--r--packages/frontend/src/components/MkUserSetupDialog.User.vue1
-rw-r--r--packages/frontend/src/components/MkVisibilityPicker.vue10
-rw-r--r--packages/frontend/src/components/MkVisitorDashboard.ActiveUsersChart.vue10
-rw-r--r--packages/frontend/src/components/MkVisitorDashboard.vue29
-rw-r--r--packages/frontend/src/components/MkWindow.vue96
-rw-r--r--packages/frontend/src/components/MkYouTubePlayer.vue15
-rw-r--r--packages/frontend/src/components/SkApprovalUser.vue9
-rw-r--r--packages/frontend/src/components/SkInstanceTicker.vue4
-rw-r--r--packages/frontend/src/components/SkNote.vue193
-rw-r--r--packages/frontend/src/components/SkNoteDetailed.vue145
-rw-r--r--packages/frontend/src/components/SkNoteSimple.vue9
-rw-r--r--packages/frontend/src/components/SkNoteSub.vue43
-rw-r--r--packages/frontend/src/components/SkOldNoteWindow.vue33
-rw-r--r--packages/frontend/src/components/form/section.vue9
-rw-r--r--packages/frontend/src/components/form/suspense.vue1
-rw-r--r--packages/frontend/src/components/global/MkA.vue10
-rw-r--r--packages/frontend/src/components/global/MkAd.stories.impl.ts3
-rw-r--r--packages/frontend/src/components/global/MkAd.vue2
-rw-r--r--packages/frontend/src/components/global/MkAvatar.vue76
-rw-r--r--packages/frontend/src/components/global/MkCustomEmoji.vue6
-rw-r--r--packages/frontend/src/components/global/MkEmoji.vue2
-rw-r--r--packages/frontend/src/components/global/MkLazy.vue53
-rw-r--r--packages/frontend/src/components/global/MkMisskeyFlavoredMarkdown.ts40
-rw-r--r--packages/frontend/src/components/global/MkPageHeader.vue36
-rw-r--r--packages/frontend/src/components/global/MkStickyContainer.vue62
-rw-r--r--packages/frontend/src/components/global/MkTime.vue61
-rw-r--r--packages/frontend/src/components/global/MkUrl.vue2
-rw-r--r--packages/frontend/src/components/global/MkUserName.stories.impl.ts1
-rw-r--r--packages/frontend/src/components/global/RouterView.vue14
-rw-r--r--packages/frontend/src/components/index.ts3
-rw-r--r--packages/frontend/src/components/page/page.text.vue1
-rw-r--r--packages/frontend/src/components/page/page.vue1
-rw-r--r--packages/frontend/src/const.ts17
-rw-r--r--packages/frontend/src/custom-emojis.ts6
-rw-r--r--packages/frontend/src/emojilist.json21
-rw-r--r--packages/frontend/src/filters/user.ts1
-rw-r--r--packages/frontend/src/index.html35
-rw-r--r--packages/frontend/src/instance.ts2
-rw-r--r--packages/frontend/src/local-storage.ts3
-rw-r--r--packages/frontend/src/navbar.ts10
-rw-r--r--packages/frontend/src/os.ts6
-rw-r--r--packages/frontend/src/pages/_error_.vue22
-rw-r--r--packages/frontend/src/pages/about-sharkey.vue36
-rw-r--r--packages/frontend/src/pages/about.emojis.vue32
-rw-r--r--packages/frontend/src/pages/about.federation.vue28
-rw-r--r--packages/frontend/src/pages/about.vue16
-rw-r--r--packages/frontend/src/pages/admin-file.vue30
-rw-r--r--packages/frontend/src/pages/admin-user.vue157
-rw-r--r--packages/frontend/src/pages/admin/_header_.vue8
-rw-r--r--packages/frontend/src/pages/admin/abuses.vue26
-rw-r--r--packages/frontend/src/pages/admin/ads.vue74
-rw-r--r--packages/frontend/src/pages/admin/announcements.vue24
-rw-r--r--packages/frontend/src/pages/admin/approvals.vue4
-rw-r--r--packages/frontend/src/pages/admin/bot-protection.vue48
-rw-r--r--packages/frontend/src/pages/admin/branding.vue85
-rw-r--r--packages/frontend/src/pages/admin/database.vue6
-rw-r--r--packages/frontend/src/pages/admin/email-settings.vue46
-rw-r--r--packages/frontend/src/pages/admin/external-services.vue19
-rw-r--r--packages/frontend/src/pages/admin/federation.vue32
-rw-r--r--packages/frontend/src/pages/admin/files.vue24
-rw-r--r--packages/frontend/src/pages/admin/index.vue109
-rw-r--r--packages/frontend/src/pages/admin/instance-block.vue19
-rw-r--r--packages/frontend/src/pages/admin/invites.vue8
-rw-r--r--packages/frontend/src/pages/admin/moderation.vue66
-rw-r--r--packages/frontend/src/pages/admin/modlog.ModLog.vue7
-rw-r--r--packages/frontend/src/pages/admin/modlog.vue16
-rw-r--r--packages/frontend/src/pages/admin/object-storage.vue82
-rw-r--r--packages/frontend/src/pages/admin/other-settings.vue42
-rw-r--r--packages/frontend/src/pages/admin/overview.active-users.vue10
-rw-r--r--packages/frontend/src/pages/admin/overview.ap-requests.vue14
-rw-r--r--packages/frontend/src/pages/admin/overview.federation.vue30
-rw-r--r--packages/frontend/src/pages/admin/overview.heatmap.vue3
-rw-r--r--packages/frontend/src/pages/admin/overview.moderators.vue10
-rw-r--r--packages/frontend/src/pages/admin/overview.queue.vue26
-rw-r--r--packages/frontend/src/pages/admin/overview.stats.vue22
-rw-r--r--packages/frontend/src/pages/admin/overview.users.vue9
-rw-r--r--packages/frontend/src/pages/admin/overview.vue44
-rw-r--r--packages/frontend/src/pages/admin/proxy-account.vue22
-rw-r--r--packages/frontend/src/pages/admin/queue.chart.vue26
-rw-r--r--packages/frontend/src/pages/admin/queue.vue11
-rw-r--r--packages/frontend/src/pages/admin/relays.vue10
-rw-r--r--packages/frontend/src/pages/admin/roles.edit.vue28
-rw-r--r--packages/frontend/src/pages/admin/roles.editor.vue76
-rw-r--r--packages/frontend/src/pages/admin/roles.role.vue16
-rw-r--r--packages/frontend/src/pages/admin/roles.vue15
-rw-r--r--packages/frontend/src/pages/admin/security.vue49
-rw-r--r--packages/frontend/src/pages/admin/server-rules.vue10
-rw-r--r--packages/frontend/src/pages/admin/settings.vue118
-rw-r--r--packages/frontend/src/pages/admin/users.vue30
-rw-r--r--packages/frontend/src/pages/ads.vue2
-rw-r--r--packages/frontend/src/pages/announcements.vue6
-rw-r--r--packages/frontend/src/pages/antenna-timeline.vue30
-rw-r--r--packages/frontend/src/pages/api-console.vue6
-rw-r--r--packages/frontend/src/pages/auth.form.vue10
-rw-r--r--packages/frontend/src/pages/auth.vue46
-rw-r--r--packages/frontend/src/pages/avatar-decorations.vue17
-rw-r--r--packages/frontend/src/pages/channel-editor.vue70
-rw-r--r--packages/frontend/src/pages/channel.vue133
-rw-r--r--packages/frontend/src/pages/channels.vue30
-rw-r--r--packages/frontend/src/pages/clip.vue56
-rw-r--r--packages/frontend/src/pages/custom-emojis-manager.vue14
-rw-r--r--packages/frontend/src/pages/drive.vue10
-rw-r--r--packages/frontend/src/pages/emoji-edit-dialog.vue68
-rw-r--r--packages/frontend/src/pages/emojis.emoji.vue2
-rw-r--r--packages/frontend/src/pages/explore.featured.vue3
-rw-r--r--packages/frontend/src/pages/explore.roles.vue6
-rw-r--r--packages/frontend/src/pages/explore.users.vue18
-rw-r--r--packages/frontend/src/pages/explore.vue12
-rw-r--r--packages/frontend/src/pages/flash/flash-edit.vue63
-rw-r--r--packages/frontend/src/pages/flash/flash-index.vue8
-rw-r--r--packages/frontend/src/pages/flash/flash.vue94
-rw-r--r--packages/frontend/src/pages/follow-requests.vue4
-rw-r--r--packages/frontend/src/pages/gallery/edit.vue46
-rw-r--r--packages/frontend/src/pages/gallery/index.vue16
-rw-r--r--packages/frontend/src/pages/gallery/post.vue52
-rw-r--r--packages/frontend/src/pages/instance-info.vue67
-rw-r--r--packages/frontend/src/pages/invite.vue2
-rw-r--r--packages/frontend/src/pages/list.vue38
-rw-r--r--packages/frontend/src/pages/miauth.vue18
-rw-r--r--packages/frontend/src/pages/my-antennas/create.vue3
-rw-r--r--packages/frontend/src/pages/my-antennas/edit.vue5
-rw-r--r--packages/frontend/src/pages/my-antennas/editor.vue60
-rw-r--r--packages/frontend/src/pages/my-antennas/index.vue8
-rw-r--r--packages/frontend/src/pages/my-clips/index.vue23
-rw-r--r--packages/frontend/src/pages/my-lists/index.vue8
-rw-r--r--packages/frontend/src/pages/my-lists/list.vue42
-rw-r--r--packages/frontend/src/pages/not-found.vue5
-rw-r--r--packages/frontend/src/pages/note.vue64
-rw-r--r--packages/frontend/src/pages/notifications.vue26
-rw-r--r--packages/frontend/src/pages/page-editor/els/page-editor.el.image.vue10
-rw-r--r--packages/frontend/src/pages/page-editor/els/page-editor.el.note.vue16
-rw-r--r--packages/frontend/src/pages/page-editor/els/page-editor.el.section.vue10
-rw-r--r--packages/frontend/src/pages/page-editor/els/page-editor.el.text.vue22
-rw-r--r--packages/frontend/src/pages/page-editor/page-editor.container.vue1
-rw-r--r--packages/frontend/src/pages/page-editor/page-editor.vue128
-rw-r--r--packages/frontend/src/pages/page.vue68
-rw-r--r--packages/frontend/src/pages/pages.vue8
-rw-r--r--packages/frontend/src/pages/registry.keys.vue16
-rw-r--r--packages/frontend/src/pages/registry.value.vue40
-rw-r--r--packages/frontend/src/pages/registry.vue9
-rw-r--r--packages/frontend/src/pages/reset-password.vue10
-rw-r--r--packages/frontend/src/pages/role.vue28
-rw-r--r--packages/frontend/src/pages/scratchpad.vue16
-rw-r--r--packages/frontend/src/pages/search.note.vue40
-rw-r--r--packages/frontend/src/pages/search.user.vue21
-rw-r--r--packages/frontend/src/pages/search.vue9
-rw-r--r--packages/frontend/src/pages/settings/2fa.vue4
-rw-r--r--packages/frontend/src/pages/settings/accounts.vue6
-rw-r--r--packages/frontend/src/pages/settings/api.vue6
-rw-r--r--packages/frontend/src/pages/settings/apps.vue6
-rw-r--r--packages/frontend/src/pages/settings/avatar-decoration.decoration.vue69
-rw-r--r--packages/frontend/src/pages/settings/avatar-decoration.dialog.vue154
-rw-r--r--packages/frontend/src/pages/settings/avatar-decoration.vue152
-rw-r--r--packages/frontend/src/pages/settings/custom-css.vue12
-rw-r--r--packages/frontend/src/pages/settings/deck.vue4
-rw-r--r--packages/frontend/src/pages/settings/drive-cleaner.vue3
-rw-r--r--packages/frontend/src/pages/settings/drive.vue10
-rw-r--r--packages/frontend/src/pages/settings/email.vue6
-rw-r--r--packages/frontend/src/pages/settings/emoji-picker.vue274
-rw-r--r--packages/frontend/src/pages/settings/general.vue68
-rw-r--r--packages/frontend/src/pages/settings/import-export.vue6
-rw-r--r--packages/frontend/src/pages/settings/index.vue81
-rw-r--r--packages/frontend/src/pages/settings/mute-block.vue48
-rw-r--r--packages/frontend/src/pages/settings/mute-block.word-mute.vue22
-rw-r--r--packages/frontend/src/pages/settings/navbar.vue5
-rw-r--r--packages/frontend/src/pages/settings/notifications.notification-config.vue8
-rw-r--r--packages/frontend/src/pages/settings/notifications.vue22
-rw-r--r--packages/frontend/src/pages/settings/other.vue4
-rw-r--r--packages/frontend/src/pages/settings/plugin.install.vue12
-rw-r--r--packages/frontend/src/pages/settings/plugin.vue6
-rw-r--r--packages/frontend/src/pages/settings/preferences-backups.vue37
-rw-r--r--packages/frontend/src/pages/settings/privacy.vue60
-rw-r--r--packages/frontend/src/pages/settings/profile.avatar-decoration-dialog.vue114
-rw-r--r--packages/frontend/src/pages/settings/profile.vue95
-rw-r--r--packages/frontend/src/pages/settings/reaction.vue196
-rw-r--r--packages/frontend/src/pages/settings/roles.vue15
-rw-r--r--packages/frontend/src/pages/settings/security.vue5
-rw-r--r--packages/frontend/src/pages/settings/sounds.sound.vue141
-rw-r--r--packages/frontend/src/pages/settings/sounds.vue42
-rw-r--r--packages/frontend/src/pages/settings/statusbar.vue10
-rw-r--r--packages/frontend/src/pages/settings/theme.install.vue14
-rw-r--r--packages/frontend/src/pages/settings/theme.manage.vue4
-rw-r--r--packages/frontend/src/pages/settings/theme.vue4
-rw-r--r--packages/frontend/src/pages/settings/webhook.edit.vue50
-rw-r--r--packages/frontend/src/pages/settings/webhook.new.vue46
-rw-r--r--packages/frontend/src/pages/settings/webhook.vue6
-rw-r--r--packages/frontend/src/pages/share.vue59
-rw-r--r--packages/frontend/src/pages/signup-complete.vue10
-rw-r--r--packages/frontend/src/pages/tag.vue4
-rw-r--r--packages/frontend/src/pages/theme-editor.vue69
-rw-r--r--packages/frontend/src/pages/timeline.vue131
-rw-r--r--packages/frontend/src/pages/user-list-timeline.vue24
-rw-r--r--packages/frontend/src/pages/user-tag.vue5
-rw-r--r--packages/frontend/src/pages/user/activity.following.vue14
-rw-r--r--packages/frontend/src/pages/user/activity.heatmap.vue18
-rw-r--r--packages/frontend/src/pages/user/activity.notes.vue14
-rw-r--r--packages/frontend/src/pages/user/activity.pv.vue14
-rw-r--r--packages/frontend/src/pages/user/followers.vue24
-rw-r--r--packages/frontend/src/pages/user/following.vue24
-rw-r--r--packages/frontend/src/pages/user/home.vue102
-rw-r--r--packages/frontend/src/pages/user/index.activity.vue12
-rw-r--r--packages/frontend/src/pages/user/index.files.vue42
-rw-r--r--packages/frontend/src/pages/user/index.timeline.vue48
-rw-r--r--packages/frontend/src/pages/user/index.vue46
-rw-r--r--packages/frontend/src/pages/user/raw.vue130
-rw-r--r--packages/frontend/src/pages/welcome.entrance.a.vue26
-rw-r--r--packages/frontend/src/pages/welcome.setup.vue18
-rw-r--r--packages/frontend/src/pages/welcome.timeline.vue19
-rw-r--r--packages/frontend/src/pages/welcome.vue10
-rw-r--r--packages/frontend/src/plugin.ts2
-rw-r--r--packages/frontend/src/router.ts10
-rw-r--r--packages/frontend/src/scripts/aiscript/api.ts2
-rw-r--r--packages/frontend/src/scripts/aiscript/ui.ts8
-rw-r--r--packages/frontend/src/scripts/api.ts55
-rw-r--r--packages/frontend/src/scripts/autocomplete.ts14
-rw-r--r--packages/frontend/src/scripts/clear-cache.ts15
-rw-r--r--packages/frontend/src/scripts/emoji-picker.ts60
-rw-r--r--packages/frontend/src/scripts/emojilist.ts6
-rw-r--r--packages/frontend/src/scripts/get-drive-file-menu.ts8
-rw-r--r--packages/frontend/src/scripts/get-note-menu.ts34
-rw-r--r--packages/frontend/src/scripts/get-user-menu.ts18
-rw-r--r--packages/frontend/src/scripts/isFfVisibleForMe.ts14
-rw-r--r--packages/frontend/src/scripts/media-has-audio.ts9
-rw-r--r--packages/frontend/src/scripts/navigator.ts8
-rw-r--r--packages/frontend/src/scripts/page-metadata.ts1
-rw-r--r--packages/frontend/src/scripts/post-message.ts25
-rw-r--r--packages/frontend/src/scripts/reaction-picker.ts9
-rw-r--r--packages/frontend/src/scripts/snowfall-effect.ts476
-rw-r--r--packages/frontend/src/scripts/sound.ts168
-rw-r--r--packages/frontend/src/scripts/theme.ts2
-rw-r--r--packages/frontend/src/store.ts74
-rw-r--r--packages/frontend/src/style.scss4
-rw-r--r--packages/frontend/src/types/menu.ts4
-rw-r--r--packages/frontend/src/types/page-header.ts11
-rw-r--r--packages/frontend/src/ui/_common_/common.ts14
-rw-r--r--packages/frontend/src/ui/_common_/common.vue8
-rw-r--r--packages/frontend/src/ui/_common_/statusbar-federation.vue6
-rw-r--r--packages/frontend/src/ui/_common_/statusbar-rss.vue4
-rw-r--r--packages/frontend/src/ui/_common_/statusbar-user-list.vue4
-rw-r--r--packages/frontend/src/ui/_common_/stream-indicator.vue8
-rw-r--r--packages/frontend/src/ui/classic.header.vue12
-rw-r--r--packages/frontend/src/ui/classic.sidebar.vue16
-rw-r--r--packages/frontend/src/ui/classic.vue42
-rw-r--r--packages/frontend/src/ui/deck.vue11
-rw-r--r--packages/frontend/src/ui/deck/antenna-column.vue4
-rw-r--r--packages/frontend/src/ui/deck/channel-column.vue11
-rw-r--r--packages/frontend/src/ui/deck/column.vue42
-rw-r--r--packages/frontend/src/ui/deck/direct-column.vue6
-rw-r--r--packages/frontend/src/ui/deck/list-column.vue10
-rw-r--r--packages/frontend/src/ui/deck/main-column.vue6
-rw-r--r--packages/frontend/src/ui/deck/mentions-column.vue6
-rw-r--r--packages/frontend/src/ui/deck/notifications-column.vue4
-rw-r--r--packages/frontend/src/ui/deck/role-timeline-column.vue4
-rw-r--r--packages/frontend/src/ui/deck/tl-column.vue28
-rw-r--r--packages/frontend/src/ui/deck/widgets-column.vue6
-rw-r--r--packages/frontend/src/ui/minimum.vue10
-rw-r--r--packages/frontend/src/ui/universal.vue30
-rw-r--r--packages/frontend/src/ui/universal.widgets.vue6
-rw-r--r--packages/frontend/src/ui/visitor.vue34
-rw-r--r--packages/frontend/src/ui/zen.vue10
-rw-r--r--packages/frontend/src/unicode-emoji-indexes/en-US.json519
-rw-r--r--packages/frontend/src/widgets/WidgetActivity.chart.vue35
-rw-r--r--packages/frontend/src/widgets/WidgetActivity.vue2
-rw-r--r--packages/frontend/src/widgets/WidgetAichan.vue3
-rw-r--r--packages/frontend/src/widgets/WidgetAiscript.vue2
-rw-r--r--packages/frontend/src/widgets/WidgetAiscriptApp.vue6
-rw-r--r--packages/frontend/src/widgets/WidgetBirthdayFollowings.vue127
-rw-r--r--packages/frontend/src/widgets/WidgetButton.vue2
-rw-r--r--packages/frontend/src/widgets/WidgetCalendar.vue2
-rw-r--r--packages/frontend/src/widgets/WidgetClicker.vue2
-rw-r--r--packages/frontend/src/widgets/WidgetClock.vue10
-rw-r--r--packages/frontend/src/widgets/WidgetDigitalClock.vue9
-rw-r--r--packages/frontend/src/widgets/WidgetFederation.vue2
-rw-r--r--packages/frontend/src/widgets/WidgetInstanceCloud.vue12
-rw-r--r--packages/frontend/src/widgets/WidgetInstanceInfo.vue2
-rw-r--r--packages/frontend/src/widgets/WidgetJobQueue.vue27
-rw-r--r--packages/frontend/src/widgets/WidgetMemo.vue2
-rw-r--r--packages/frontend/src/widgets/WidgetNotifications.vue2
-rw-r--r--packages/frontend/src/widgets/WidgetOnlineUsers.vue2
-rw-r--r--packages/frontend/src/widgets/WidgetPhotos.vue2
-rw-r--r--packages/frontend/src/widgets/WidgetPostForm.vue2
-rw-r--r--packages/frontend/src/widgets/WidgetProfile.vue2
-rw-r--r--packages/frontend/src/widgets/WidgetRss.vue10
-rw-r--r--packages/frontend/src/widgets/WidgetRssTicker.vue14
-rw-r--r--packages/frontend/src/widgets/WidgetSearch.vue48
-rw-r--r--packages/frontend/src/widgets/WidgetSlideshow.vue2
-rw-r--r--packages/frontend/src/widgets/WidgetTimeline.vue4
-rw-r--r--packages/frontend/src/widgets/WidgetTrends.vue2
-rw-r--r--packages/frontend/src/widgets/WidgetUnixClock.vue2
-rw-r--r--packages/frontend/src/widgets/WidgetUserList.vue19
-rw-r--r--packages/frontend/src/widgets/index.ts2
-rw-r--r--packages/frontend/src/widgets/server-metric/cpu-mem.vue56
-rw-r--r--packages/frontend/src/widgets/server-metric/cpu.vue6
-rw-r--r--packages/frontend/src/widgets/server-metric/disk.vue10
-rw-r--r--packages/frontend/src/widgets/server-metric/mem.vue18
-rw-r--r--packages/frontend/src/widgets/server-metric/net.vue60
-rw-r--r--packages/frontend/src/widgets/server-metric/pie.vue6
401 files changed, 7678 insertions, 5121 deletions
diff --git a/packages/frontend/src/_dev_boot_.ts b/packages/frontend/src/_dev_boot_.ts
new file mode 100644
index 0000000000..d419ade527
--- /dev/null
+++ b/packages/frontend/src/_dev_boot_.ts
@@ -0,0 +1,95 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and other misskey contributors
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+// devモードで起動される際(index.htmlを使うとき)はrouterが暴発してしまってうまく読み込めない。
+// よって、devモードとして起動されるときはビルド時に組み込む形としておく。
+// (pnpm start時はpugファイルの中で静的リソースとして読み込むようになっており、この問題は起こっていない)
+import '@phosphor-icons/web/bold';
+
+await main();
+
+import('@/_boot_.js');
+
+/**
+ * backend/src/server/web/boot.jsで差し込まれている起動処理のうち、最低限必要なものを模倣するための処理
+ */
+async function main() {
+ const forceError = localStorage.getItem('forceError');
+ if (forceError != null) {
+ renderError('FORCED_ERROR', 'This error is forced by having forceError in local storage.');
+ }
+
+ //#region Detect language & fetch translations
+
+ // dev-modeの場合は常に取り直す
+ const supportedLangs = _LANGS_.map(it => it[0]);
+ let lang: string | null | undefined = localStorage.getItem('lang');
+ if (lang == null || !supportedLangs.includes(lang)) {
+ if (supportedLangs.includes(navigator.language)) {
+ lang = navigator.language;
+ } else {
+ lang = supportedLangs.find(x => x.split('-')[0] === navigator.language);
+
+ // Fallback
+ if (lang == null) lang = 'en-US';
+ }
+ }
+
+ // TODO:今のままだと言語ファイル変更後はpnpm devをリスタートする必要があるので、chokidarを使ったり等で対応できるようにする
+ const locale = _LANGS_FULL_.find(it => it[0] === lang);
+ localStorage.setItem('lang', lang);
+ localStorage.setItem('locale', JSON.stringify(locale[1]));
+ localStorage.setItem('localeVersion', _VERSION_);
+ //#endregion
+
+ //#region Theme
+ const theme = localStorage.getItem('theme');
+ if (theme) {
+ for (const [k, v] of Object.entries(JSON.parse(theme))) {
+ document.documentElement.style.setProperty(`--${k}`, v.toString());
+
+ // HTMLの theme-color 適用
+ if (k === 'htmlThemeColor') {
+ for (const tag of document.head.children) {
+ if (tag.tagName === 'META' && tag.getAttribute('name') === 'theme-color') {
+ tag.setAttribute('content', v);
+ break;
+ }
+ }
+ }
+ }
+ }
+ const colorScheme = localStorage.getItem('colorScheme');
+ if (colorScheme) {
+ document.documentElement.style.setProperty('color-scheme', colorScheme);
+ }
+ //#endregion
+
+ const fontSize = localStorage.getItem('fontSize');
+ if (fontSize) {
+ document.documentElement.classList.add('f-' + fontSize);
+ }
+
+ const useSystemFont = localStorage.getItem('useSystemFont');
+ if (useSystemFont) {
+ document.documentElement.classList.add('useSystemFont');
+ }
+
+ const wallpaper = localStorage.getItem('wallpaper');
+ if (wallpaper) {
+ document.documentElement.style.backgroundImage = `url(${wallpaper})`;
+ }
+
+ const customCss = localStorage.getItem('customCss');
+ if (customCss && customCss.length > 0) {
+ const style = document.createElement('style');
+ style.innerHTML = customCss;
+ document.head.appendChild(style);
+ }
+}
+
+function renderError(code: string, details?: string) {
+ console.log(code, details);
+}
diff --git a/packages/frontend/src/account.ts b/packages/frontend/src/account.ts
index f19cad331b..05008194f0 100644
--- a/packages/frontend/src/account.ts
+++ b/packages/frontend/src/account.ts
@@ -16,7 +16,7 @@ import { unisonReload, reloadChannel } from '@/scripts/unison-reload.js';
// TODO: 他のタブと永続化されたstateを同期
-type Account = Misskey.entities.MeDetailed;
+type Account = Misskey.entities.MeDetailed & { token: string };
const accountData = miLocalStorage.getItem('account');
@@ -284,7 +284,7 @@ export async function openAccountMenu(opts: {
text: i18n.ts.profile,
to: `/@${ $i.username }`,
avatar: $i,
- }, null, ...(opts.includeCurrentAccount ? [createItem($i)] : []), ...accountItemPromises, {
+ }, { type: 'divider' }, ...(opts.includeCurrentAccount ? [createItem($i)] : []), ...accountItemPromises, {
type: 'parent' as const,
icon: 'ph-plus ph-bold ph-lg',
text: i18n.ts.addAccount,
diff --git a/packages/frontend/src/boot/common.ts b/packages/frontend/src/boot/common.ts
index 82bb44c071..63f169d9ad 100644
--- a/packages/frontend/src/boot/common.ts
+++ b/packages/frontend/src/boot/common.ts
@@ -3,28 +3,25 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
-import { computed, createApp, watch, markRaw, version as vueVersion, defineAsyncComponent, App } from 'vue';
+import { computed, watch, version as vueVersion, App } from 'vue';
import { compareVersions } from 'compare-versions';
import widgets from '@/widgets/index.js';
import directives from '@/directives/index.js';
import components from '@/components/index.js';
-import { version, ui, lang, updateLocale } from '@/config.js';
+import { version, lang, updateLocale, locale } from '@/config.js';
import { applyTheme } from '@/scripts/theme.js';
import { isDeviceDarkmode } from '@/scripts/is-device-darkmode.js';
-import { i18n, updateI18n } from '@/i18n.js';
-import { confirm, alert, post, popup, toast } from '@/os.js';
-import { $i, refreshAccount, login, updateAccount, signout } from '@/account.js';
+import { updateI18n } from '@/i18n.js';
+import { $i, refreshAccount, login } from '@/account.js';
import { defaultStore, ColdDeviceStorage } from '@/store.js';
import { fetchInstance, instance } from '@/instance.js';
import { deviceKind } from '@/scripts/device-kind.js';
import { reloadChannel } from '@/scripts/unison-reload.js';
-import { reactionPicker } from '@/scripts/reaction-picker.js';
import { getUrlWithoutLoginId } from '@/scripts/login-id.js';
import { getAccountFromId } from '@/scripts/get-account-from-id.js';
import { deckStore } from '@/ui/deck/deck-store.js';
import { miLocalStorage } from '@/local-storage.js';
import { fetchCustomEmojis } from '@/custom-emojis.js';
-import { mainRouter } from '@/router.js';
export async function common(createVue: () => App<Element>) {
console.info(`Sharkey v${version}`);
@@ -88,7 +85,7 @@ export async function common(createVue: () => App<Element>) {
//#region Detect language & fetch translations
const localeVersion = miLocalStorage.getItem('localeVersion');
- const localeOutdated = (localeVersion == null || localeVersion !== version);
+ const localeOutdated = (localeVersion == null || localeVersion !== version || locale == null);
if (localeOutdated) {
const res = await window.fetch(`/assets/locales/${lang}.${version}.json`);
if (res.status === 200) {
@@ -187,6 +184,12 @@ export async function common(createVue: () => App<Element>) {
if (instance.defaultLightTheme != null) ColdDeviceStorage.set('lightTheme', JSON.parse(instance.defaultLightTheme));
if (instance.defaultDarkTheme != null) ColdDeviceStorage.set('darkTheme', JSON.parse(instance.defaultDarkTheme));
defaultStore.set('themeInitial', false);
+ } else {
+ if (defaultStore.state.darkMode) {
+ applyTheme(darkTheme.value);
+ } else {
+ applyTheme(lightTheme.value);
+ }
}
});
@@ -202,24 +205,28 @@ export async function common(createVue: () => App<Element>) {
}
}, { immediate: true });
- if (defaultStore.state.keepScreenOn) {
- if ('wakeLock' in navigator) {
+ // Keep screen on
+ const onVisibilityChange = () => document.addEventListener('visibilitychange', () => {
+ if (document.visibilityState === 'visible') {
try {
navigator.wakeLock.request('screen');
} catch (err) {
return;
}
-
- document.addEventListener('visibilitychange', async () => {
- if (document.visibilityState === 'visible') {
- try {
- navigator.wakeLock.request('screen');
- } catch (err) {
- return;
- }
- }
- });
}
+ });
+ if (defaultStore.state.keepScreenOn && 'wakeLock' in navigator) {
+ navigator.wakeLock.request('screen')
+ .then(onVisibilityChange)
+ .catch(() => {
+ // On WebKit-based browsers, user activation is required to send wake lock request
+ // https://webkit.org/blog/13862/the-user-activation-api/
+ document.addEventListener(
+ 'click',
+ () => navigator.wakeLock.request('screen').then(onVisibilityChange),
+ { once: true },
+ );
+ });
}
//#region Fetch user
diff --git a/packages/frontend/src/boot/main-boot.ts b/packages/frontend/src/boot/main-boot.ts
index 72ae8d64a3..cdc1d11ca2 100644
--- a/packages/frontend/src/boot/main-boot.ts
+++ b/packages/frontend/src/boot/main-boot.ts
@@ -3,14 +3,14 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
-import { computed, createApp, watch, markRaw, version as vueVersion, defineAsyncComponent } from 'vue';
+import { createApp, markRaw, defineAsyncComponent } from 'vue';
import { common } from './common.js';
-import { version, ui, lang, updateLocale } from '@/config.js';
-import { i18n, updateI18n } from '@/i18n.js';
+import { ui } from '@/config.js';
+import { i18n } from '@/i18n.js';
import { confirm, alert, post, popup, toast } from '@/os.js';
import { useStream } from '@/stream.js';
import * as sound from '@/scripts/sound.js';
-import { $i, refreshAccount, login, updateAccount, signout } from '@/account.js';
+import { $i, updateAccount, signout } from '@/account.js';
import { defaultStore, ColdDeviceStorage } from '@/store.js';
import { makeHotkey } from '@/scripts/hotkey.js';
import { reactionPicker } from '@/scripts/reaction-picker.js';
@@ -19,6 +19,7 @@ import { claimAchievement, claimedAchievements } from '@/scripts/achievements.js
import { mainRouter } from '@/router.js';
import { initializeSw } from '@/scripts/initialize-sw.js';
import { deckStore } from '@/ui/deck/deck-store.js';
+import { emojiPicker } from '@/scripts/emoji-picker.js';
export async function mainBoot() {
const { isClientUpdated } = await common(() => createApp(
@@ -30,6 +31,7 @@ export async function mainBoot() {
));
reactionPicker.init();
+ emojiPicker.init();
if (isClientUpdated && $i) {
popup(defineAsyncComponent(() => import('@/components/MkUpdated.vue')), {}, {}, 'closed');
@@ -71,6 +73,14 @@ export async function mainBoot() {
},
};
+ if (defaultStore.state.enableSeasonalScreenEffect) {
+ const month = new Date().getMonth() + 1;
+ if (month === 12 || month === 1) {
+ const SnowfallEffect = (await import('@/scripts/snowfall-effect.js')).SnowfallEffect;
+ new SnowfallEffect().render();
+ }
+ }
+
if ($i) {
// only add post shortcuts if logged in
hotkeys['p|n'] = post;
diff --git a/packages/frontend/src/boot/sub-boot.ts b/packages/frontend/src/boot/sub-boot.ts
index 9b4670e130..92ee074afb 100644
--- a/packages/frontend/src/boot/sub-boot.ts
+++ b/packages/frontend/src/boot/sub-boot.ts
@@ -3,7 +3,7 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
-import { computed, createApp, watch, markRaw, version as vueVersion, defineAsyncComponent } from 'vue';
+import { createApp, defineAsyncComponent } from 'vue';
import { common } from './common.js';
export async function subBoot() {
diff --git a/packages/frontend/src/components/MkAbuseReport.vue b/packages/frontend/src/components/MkAbuseReport.vue
index 14247f4bf5..611c8a1782 100644
--- a/packages/frontend/src/components/MkAbuseReport.vue
+++ b/packages/frontend/src/components/MkAbuseReport.vue
@@ -23,7 +23,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<Mfm :text="report.comment"/>
</div>
<hr/>
- <div>{{ i18n.ts.reporter }}: <MkAcct :user="report.reporter"/></div>
+ <div>{{ i18n.ts.reporter }}: <MkA :to="`/admin/user/${report.reporter.id}`" class="_link">@{{ report.reporter.username }}</MkA></div>
<div v-if="report.assignee">
{{ i18n.ts.moderator }}:
<MkAcct :user="report.assignee"/>
@@ -41,6 +41,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
+import { ref } from 'vue';
import MkButton from '@/components/MkButton.vue';
import MkSwitch from '@/components/MkSwitch.vue';
import MkKeyValue from '@/components/MkKeyValue.vue';
@@ -56,11 +57,11 @@ const emit = defineEmits<{
(ev: 'resolved', reportId: string): void;
}>();
-let forward = $ref(props.report.forwarded);
+const forward = ref(props.report.forwarded);
function resolve() {
os.apiWithDialog('admin/resolve-abuse-user-report', {
- forward: forward,
+ forward: forward.value,
reportId: props.report.id,
}).then(() => {
emit('resolved', props.report.id);
diff --git a/packages/frontend/src/components/MkAchievements.vue b/packages/frontend/src/components/MkAchievements.vue
index aac7f508a1..40f9ad4057 100644
--- a/packages/frontend/src/components/MkAchievements.vue
+++ b/packages/frontend/src/components/MkAchievements.vue
@@ -53,7 +53,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import * as Misskey from 'misskey-js';
-import { onMounted } from 'vue';
+import { onMounted, ref, computed } from 'vue';
import * as os from '@/os.js';
import { i18n } from '@/i18n.js';
import { ACHIEVEMENT_TYPES, ACHIEVEMENT_BADGES, claimAchievement } from '@/scripts/achievements.js';
@@ -67,15 +67,15 @@ const props = withDefaults(defineProps<{
withDescription: true,
});
-let achievements = $ref();
-const lockedAchievements = $computed(() => ACHIEVEMENT_TYPES.filter(x => !(achievements ?? []).some(a => a.name === x)));
+const achievements = ref();
+const lockedAchievements = computed(() => ACHIEVEMENT_TYPES.filter(x => !(achievements.value ?? []).some(a => a.name === x)));
function fetch() {
os.api('users/achievements', { userId: props.user.id }).then(res => {
- achievements = [];
+ achievements.value = [];
for (const t of ACHIEVEMENT_TYPES) {
const a = res.find(x => x.name === t);
- if (a) achievements.push(a);
+ if (a) achievements.value.push(a);
}
//achievements = res.sort((a, b) => b.unlockedAt - a.unlockedAt);
});
diff --git a/packages/frontend/src/components/MkAnalogClock.vue b/packages/frontend/src/components/MkAnalogClock.vue
index cd2c4d8264..0e252f7b1d 100644
--- a/packages/frontend/src/components/MkAnalogClock.vue
+++ b/packages/frontend/src/components/MkAnalogClock.vue
@@ -138,45 +138,45 @@ const texts = computed(() => {
});
let enabled = true;
-let majorGraduationColor = $ref<string>();
+const majorGraduationColor = ref<string>();
//let minorGraduationColor = $ref<string>();
-let sHandColor = $ref<string>();
-let mHandColor = $ref<string>();
-let hHandColor = $ref<string>();
-let nowColor = $ref<string>();
-let h = $ref<number>(0);
-let m = $ref<number>(0);
-let s = $ref<number>(0);
-let hAngle = $ref<number>(0);
-let mAngle = $ref<number>(0);
-let sAngle = $ref<number>(0);
-let disableSAnimate = $ref(false);
+const sHandColor = ref<string>();
+const mHandColor = ref<string>();
+const hHandColor = ref<string>();
+const nowColor = ref<string>();
+const h = ref<number>(0);
+const m = ref<number>(0);
+const s = ref<number>(0);
+const hAngle = ref<number>(0);
+const mAngle = ref<number>(0);
+const sAngle = ref<number>(0);
+const disableSAnimate = ref(false);
let sOneRound = false;
const sLine = ref<SVGPathElement>();
function tick() {
const now = props.now();
now.setMinutes(now.getMinutes() + now.getTimezoneOffset() + props.offset);
- const previousS = s;
- const previousM = m;
- const previousH = h;
- s = now.getSeconds();
- m = now.getMinutes();
- h = now.getHours();
- if (previousS === s && previousM === m && previousH === h) {
+ const previousS = s.value;
+ const previousM = m.value;
+ const previousH = h.value;
+ s.value = now.getSeconds();
+ m.value = now.getMinutes();
+ h.value = now.getHours();
+ if (previousS === s.value && previousM === m.value && previousH === h.value) {
return;
}
- hAngle = Math.PI * (h % (props.twentyfour ? 24 : 12) + (m + s / 60) / 60) / (props.twentyfour ? 12 : 6);
- mAngle = Math.PI * (m + s / 60) / 30;
+ hAngle.value = Math.PI * (h.value % (props.twentyfour ? 24 : 12) + (m.value + s.value / 60) / 60) / (props.twentyfour ? 12 : 6);
+ mAngle.value = Math.PI * (m.value + s.value / 60) / 30;
if (sOneRound && sLine.value) { // 秒針が一周した際のアニメーションをよしなに処理する(これが無いと秒が59->0になったときに期待したアニメーションにならない)
- sAngle = Math.PI * 60 / 30;
+ sAngle.value = Math.PI * 60 / 30;
defaultIdlingRenderScheduler.delete(tick);
sLine.value.addEventListener('transitionend', () => {
- disableSAnimate = true;
+ disableSAnimate.value = true;
requestAnimationFrame(() => {
- sAngle = 0;
+ sAngle.value = 0;
requestAnimationFrame(() => {
- disableSAnimate = false;
+ disableSAnimate.value = false;
if (enabled) {
defaultIdlingRenderScheduler.add(tick);
}
@@ -184,9 +184,9 @@ function tick() {
});
}, { once: true });
} else {
- sAngle = Math.PI * s / 30;
+ sAngle.value = Math.PI * s.value / 30;
}
- sOneRound = s === 59;
+ sOneRound = s.value === 59;
}
tick();
@@ -195,12 +195,12 @@ function calcColors() {
const computedStyle = getComputedStyle(document.documentElement);
const dark = tinycolor(computedStyle.getPropertyValue('--bg')).isDark();
const accent = tinycolor(computedStyle.getPropertyValue('--accent')).toHexString();
- majorGraduationColor = dark ? 'rgba(255, 255, 255, 0.3)' : 'rgba(0, 0, 0, 0.3)';
+ majorGraduationColor.value = dark ? 'rgba(255, 255, 255, 0.3)' : 'rgba(0, 0, 0, 0.3)';
//minorGraduationColor = dark ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)';
- sHandColor = dark ? 'rgba(255, 255, 255, 0.5)' : 'rgba(0, 0, 0, 0.3)';
- mHandColor = tinycolor(computedStyle.getPropertyValue('--fg')).toHexString();
- hHandColor = accent;
- nowColor = accent;
+ sHandColor.value = dark ? 'rgba(255, 255, 255, 0.5)' : 'rgba(0, 0, 0, 0.3)';
+ mHandColor.value = tinycolor(computedStyle.getPropertyValue('--fg')).toHexString();
+ hHandColor.value = accent;
+ nowColor.value = accent;
}
calcColors();
diff --git a/packages/frontend/src/components/MkAnimBg.vue b/packages/frontend/src/components/MkAnimBg.vue
index 70d101a9d3..284ee8f3f8 100644
--- a/packages/frontend/src/components/MkAnimBg.vue
+++ b/packages/frontend/src/components/MkAnimBg.vue
@@ -21,8 +21,9 @@ const props = withDefaults(defineProps<{
focus: 1.0,
});
-function loadShader(gl, type, source) {
+function loadShader(gl: WebGLRenderingContext, type: number, source: string) {
const shader = gl.createShader(type);
+ if (shader == null) return null;
gl.shaderSource(shader, source);
gl.compileShader(shader);
@@ -38,11 +39,13 @@ function loadShader(gl, type, source) {
return shader;
}
-function initShaderProgram(gl, vsSource, fsSource) {
+function initShaderProgram(gl: WebGLRenderingContext, vsSource: string, fsSource: string) {
const vertexShader = loadShader(gl, gl.VERTEX_SHADER, vsSource);
const fragmentShader = loadShader(gl, gl.FRAGMENT_SHADER, fsSource);
const shaderProgram = gl.createProgram();
+ if (shaderProgram == null || vertexShader == null || fragmentShader == null) return null;
+
gl.attachShader(shaderProgram, vertexShader);
gl.attachShader(shaderProgram, fragmentShader);
gl.linkProgram(shaderProgram);
@@ -63,8 +66,10 @@ let handle: ReturnType<typeof window['requestAnimationFrame']> | null = null;
onMounted(() => {
const canvas = canvasEl.value!;
- canvas.width = canvas.offsetWidth;
- canvas.height = canvas.offsetHeight;
+ let width = canvas.offsetWidth;
+ let height = canvas.offsetHeight;
+ canvas.width = width;
+ canvas.height = height;
const gl = canvas.getContext('webgl', { premultipliedAlpha: true });
if (gl == null) return;
@@ -197,6 +202,7 @@ onMounted(() => {
gl_FragColor = vec4( color, max(max(color.x, color.y), color.z) );
}
`);
+ if (shaderProgram == null) return;
gl.useProgram(shaderProgram);
const u_resolution = gl.getUniformLocation(shaderProgram, 'u_resolution');
@@ -226,7 +232,23 @@ onMounted(() => {
gl!.uniform1f(u_time, 0);
gl!.drawArrays(gl!.TRIANGLE_STRIP, 0, 4);
} else {
- function render(timeStamp) {
+ function render(timeStamp: number) {
+ let sizeChanged = false;
+ if (Math.abs(height - canvas.offsetHeight) > 2) {
+ height = canvas.offsetHeight;
+ canvas.height = height;
+ sizeChanged = true;
+ }
+ if (Math.abs(width - canvas.offsetWidth) > 2) {
+ width = canvas.offsetWidth;
+ canvas.width = width;
+ sizeChanged = true;
+ }
+ if (sizeChanged && gl) {
+ gl.uniform2fv(u_resolution, [width, height]);
+ gl.viewport(0, 0, width, height);
+ }
+
gl!.uniform1f(u_time, timeStamp);
gl!.drawArrays(gl!.TRIANGLE_STRIP, 0, 4);
diff --git a/packages/frontend/src/components/MkAsUi.vue b/packages/frontend/src/components/MkAsUi.vue
index 9596ce6077..60978eb0bd 100644
--- a/packages/frontend/src/components/MkAsUi.vue
+++ b/packages/frontend/src/components/MkAsUi.vue
@@ -43,6 +43,7 @@ SPDX-License-Identifier: AGPL-3.0-only
fixed
:instant="true"
:initialText="c.form.text"
+ :initialCw="c.form.cw"
/>
</div>
<MkFolder v-else-if="c.type === 'folder'" :defaultOpen="c.opened">
@@ -60,7 +61,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { Ref } from 'vue';
+import { Ref, ref } from 'vue';
import * as os from '@/os.js';
import MkButton from '@/components/MkButton.vue';
import MkInput from '@/components/MkInput.vue';
@@ -87,16 +88,17 @@ function g(id) {
return props.components.find(x => x.value.id === id).value;
}
-let valueForSwitch = $ref(c.default ?? false);
+const valueForSwitch = ref(c.default ?? false);
function onSwitchUpdate(v) {
- valueForSwitch = v;
+ valueForSwitch.value = v;
if (c.onChange) c.onChange(v);
}
function openPostForm() {
os.post({
initialText: c.form.text,
+ initialCw: c.form.cw,
instant: true,
});
}
diff --git a/packages/frontend/src/components/MkAutocomplete.vue b/packages/frontend/src/components/MkAutocomplete.vue
index 9e92c4bb03..1f819cf601 100644
--- a/packages/frontend/src/components/MkAutocomplete.vue
+++ b/packages/frontend/src/components/MkAutocomplete.vue
@@ -242,29 +242,7 @@ function exec() {
return;
}
- const matched: EmojiDef[] = [];
- const max = 30;
-
- emojiDb.value.some(x => {
- if (x.name.toLowerCase().startsWith(props.q ? props.q.toLowerCase() : '') && !x.aliasOf && !matched.some(y => y.emoji.toLowerCase() === x.emoji.toLowerCase())) matched.push(x);
- return matched.length === max;
- });
-
- if (matched.length < max) {
- emojiDb.value.some(x => {
- if (x.name.toLowerCase().startsWith(props.q ? props.q.toLowerCase() : '') && !matched.some(y => y.emoji.toLowerCase() === x.emoji.toLowerCase())) matched.push(x);
- return matched.length === max;
- });
- }
-
- if (matched.length < max) {
- emojiDb.value.some(x => {
- if (x.name.toLowerCase().includes(props.q ? props.q.toLowerCase() : '') && !matched.some(y => y.emoji.toLowerCase() === x.emoji.toLowerCase())) matched.push(x);
- return matched.length === max;
- });
- }
-
- emojis.value = matched;
+ emojis.value = emojiAutoComplete(props.q.toLowerCase(), emojiDb.value);
} else if (props.type === 'mfmTag') {
if (!props.q || props.q === '') {
mfmTags.value = MFM_TAGS;
@@ -275,6 +253,78 @@ function exec() {
}
}
+type EmojiScore = { emoji: EmojiDef, score: number };
+
+function emojiAutoComplete(query: string | null, emojiDb: EmojiDef[], max = 30): EmojiDef[] {
+ if (!query) {
+ return [];
+ }
+
+ const matched = new Map<string, EmojiScore>();
+
+ // 前方一致(エイリアスなし)
+ emojiDb.some(x => {
+ if (x.name.toLowerCase().startsWith(query) && !x.aliasOf) {
+ matched.set(x.name, { emoji: x, score: query.length + 1 });
+ }
+ return matched.size === max;
+ });
+
+ // 前方一致(エイリアス込み)
+ if (matched.size < max) {
+ emojiDb.some(x => {
+ if (x.name.toLowerCase().startsWith(query) && !matched.has(x.aliasOf ?? x.name)) {
+ matched.set(x.aliasOf ?? x.name, { emoji: x, score: query.length });
+ }
+ return matched.size === max;
+ });
+ }
+
+ // 部分一致(エイリアス込み)
+ if (matched.size < max) {
+ emojiDb.some(x => {
+ if (x.name.toLowerCase().includes(query) && !matched.has(x.aliasOf ?? x.name)) {
+ matched.set(x.aliasOf ?? x.name, { emoji: x, score: query.length - 1 });
+ }
+ return matched.size === max;
+ });
+ }
+
+ // 簡易あいまい検索(3文字以上)
+ if (matched.size < max && query.length > 3) {
+ const queryChars = [...query];
+ const hitEmojis = new Map<string, EmojiScore>();
+
+ for (const x of emojiDb) {
+ // 文字列の位置を進めながら、クエリの文字を順番に探す
+
+ let pos = 0;
+ let hit = 0;
+ for (const c of queryChars) {
+ pos = x.name.toLowerCase().indexOf(c, pos);
+ if (pos <= -1) break;
+ hit++;
+ }
+
+ // 半分以上の文字が含まれていればヒットとする
+ if (hit > Math.ceil(queryChars.length / 2) && hit - 2 > (matched.get(x.aliasOf ?? x.name)?.score ?? 0)) {
+ hitEmojis.set(x.aliasOf ?? x.name, { emoji: x, score: hit - 2 });
+ }
+ }
+
+ // ヒットしたものを全部追加すると雑多になるので、先頭の6件程度だけにしておく(6件=オートコンプリートのポップアップのサイズ分)
+ [...hitEmojis.values()]
+ .sort((x, y) => y.score - x.score)
+ .slice(0, 6)
+ .forEach(it => matched.set(it.emoji.name, it));
+ }
+
+ return [...matched.values()]
+ .sort((x, y) => y.score - x.score)
+ .slice(0, max)
+ .map(it => it.emoji);
+}
+
function onMousedown(event: Event) {
if (!contains(rootEl.value, event.target) && (rootEl.value !== event.target)) props.close();
}
@@ -309,12 +359,25 @@ function onKeydown(event: KeyboardEvent) {
}
break;
- case 'Tab':
case 'ArrowDown':
cancel();
selectNext();
break;
+ case 'Tab':
+ if (event.shiftKey) {
+ if (select.value !== -1) {
+ cancel();
+ selectPrev();
+ } else {
+ props.close();
+ }
+ } else {
+ cancel();
+ selectNext();
+ }
+ break;
+
default:
event.stopPropagation();
props.textarea.focus();
diff --git a/packages/frontend/src/components/MkButton.vue b/packages/frontend/src/components/MkButton.vue
index 2fdc2bbe07..9fcc49d3f0 100644
--- a/packages/frontend/src/components/MkButton.vue
+++ b/packages/frontend/src/components/MkButton.vue
@@ -33,7 +33,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { nextTick, onMounted } from 'vue';
+import { nextTick, onMounted, shallowRef } from 'vue';
const props = defineProps<{
type?: 'button' | 'submit' | 'reset';
@@ -59,13 +59,13 @@ const emit = defineEmits<{
(ev: 'click', payload: MouseEvent): void;
}>();
-let el = $shallowRef<HTMLElement | null>(null);
-let ripples = $shallowRef<HTMLElement | null>(null);
+const el = shallowRef<HTMLElement | null>(null);
+const ripples = shallowRef<HTMLElement | null>(null);
onMounted(() => {
if (props.autofocus) {
nextTick(() => {
- el!.focus();
+ el.value!.focus();
});
}
});
@@ -88,11 +88,11 @@ function onMousedown(evt: MouseEvent): void {
const rect = target.getBoundingClientRect();
const ripple = document.createElement('div');
- ripple.classList.add(ripples!.dataset.childrenClass!);
+ ripple.classList.add(ripples.value!.dataset.childrenClass!);
ripple.style.top = (evt.clientY - rect.top - 1).toString() + 'px';
ripple.style.left = (evt.clientX - rect.left - 1).toString() + 'px';
- ripples!.appendChild(ripple);
+ ripples.value!.appendChild(ripple);
const circleCenterX = evt.clientX - rect.left;
const circleCenterY = evt.clientY - rect.top;
@@ -107,7 +107,7 @@ function onMousedown(evt: MouseEvent): void {
ripple.style.opacity = '0';
}, 1000);
window.setTimeout(() => {
- if (ripples) ripples.removeChild(ripple);
+ if (ripples.value) ripples.value.removeChild(ripple);
}, 2000);
}
</script>
diff --git a/packages/frontend/src/components/MkChannelPreview.vue b/packages/frontend/src/components/MkChannelPreview.vue
index 9c6e2f00bd..96590a469b 100644
--- a/packages/frontend/src/components/MkChannelPreview.vue
+++ b/packages/frontend/src/components/MkChannelPreview.vue
@@ -4,49 +4,70 @@ SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
-<MkA :to="`/channels/${channel.id}`" class="eftoefju _panel" tabindex="-1">
- <div class="banner" :style="bannerStyle">
- <div class="fade"></div>
- <div class="name"><i class="ph-television ph-bold ph-lg"></i> {{ channel.name }}</div>
- <div v-if="channel.isSensitive" class="sensitiveIndicator">{{ i18n.ts.sensitive }}</div>
- <div class="status">
- <div>
- <i class="ph-users ph-bold ph-lg"></i>
- <I18n :src="i18n.ts._channel.usersCount" tag="span" style="margin-left: 4px;">
- <template #n>
- <b>{{ channel.usersCount }}</b>
- </template>
- </I18n>
- </div>
- <div>
- <i class="ph-pencil ph-bold ph-lg"></i>
- <I18n :src="i18n.ts._channel.notesCount" tag="span" style="margin-left: 4px;">
- <template #n>
- <b>{{ channel.notesCount }}</b>
- </template>
- </I18n>
+<div style="position: relative;">
+ <MkA :to="`/channels/${channel.id}`" class="eftoefju _panel" tabindex="-1" @click="updateLastReadedAt">
+ <div class="banner" :style="bannerStyle">
+ <div class="fade"></div>
+ <div class="name"><i class="ph-television ph-bold ph-lg"></i> {{ channel.name }}</div>
+ <div v-if="channel.isSensitive" class="sensitiveIndicator">{{ i18n.ts.sensitive }}</div>
+ <div class="status">
+ <div>
+ <i class="ph-users ph-bold ph-lg"></i>
+ <I18n :src="i18n.ts._channel.usersCount" tag="span" style="margin-left: 4px;">
+ <template #n>
+ <b>{{ channel.usersCount }}</b>
+ </template>
+ </I18n>
+ </div>
+ <div>
+ <i class="ph-pencil ph-bold ph-lg"></i>
+ <I18n :src="i18n.ts._channel.notesCount" tag="span" style="margin-left: 4px;">
+ <template #n>
+ <b>{{ channel.notesCount }}</b>
+ </template>
+ </I18n>
+ </div>
</div>
</div>
- </div>
- <article v-if="channel.description">
- <p :title="channel.description">{{ channel.description.length > 85 ? channel.description.slice(0, 85) + '…' : channel.description }}</p>
- </article>
- <footer>
- <span v-if="channel.lastNotedAt">
- {{ i18n.ts.updatedAt }}: <MkTime :time="channel.lastNotedAt"/>
- </span>
- </footer>
-</MkA>
+ <article v-if="channel.description">
+ <p :title="channel.description">{{ channel.description.length > 85 ? channel.description.slice(0, 85) + '…' : channel.description }}</p>
+ </article>
+ <footer>
+ <span v-if="channel.lastNotedAt">
+ {{ i18n.ts.updatedAt }}: <MkTime :time="channel.lastNotedAt"/>
+ </span>
+ </footer>
+ </MkA>
+ <div
+ v-if="channel.lastNotedAt && (channel.isFavorited || channel.isFollowing) && (!lastReadedAt || Date.parse(channel.lastNotedAt) > lastReadedAt)"
+ class="indicator"
+ ></div>
+</div>
</template>
<script lang="ts" setup>
-import { computed } from 'vue';
+import { computed, ref, watch } from 'vue';
import { i18n } from '@/i18n.js';
+import { miLocalStorage } from '@/local-storage.js';
const props = defineProps<{
channel: Record<string, any>;
}>();
+const getLastReadedAt = (): number | null => {
+ return miLocalStorage.getItemAsJson(`channelLastReadedAt:${props.channel.id}`) ?? null;
+};
+
+const lastReadedAt = ref(getLastReadedAt());
+
+watch(() => props.channel.id, () => {
+ lastReadedAt.value = getLastReadedAt();
+});
+
+const updateLastReadedAt = () => {
+ lastReadedAt.value = props.channel.lastNotedAt ? Date.parse(props.channel.lastNotedAt) : Date.now();
+};
+
const bannerStyle = computed(() => {
if (props.channel.bannerUrl) {
return { backgroundImage: `url(${props.channel.bannerUrl})` };
@@ -170,4 +191,17 @@ const bannerStyle = computed(() => {
}
}
+.indicator {
+ position: absolute;
+ top: 0;
+ right: 0;
+ transform: translate(25%, -25%);
+ background-color: var(--accent);
+ border: solid var(--bg) 4px;
+ border-radius: 100%;
+ width: 1.5rem;
+ height: 1.5rem;
+ aspect-ratio: 1 / 1;
+}
+
</style>
diff --git a/packages/frontend/src/components/MkChart.vue b/packages/frontend/src/components/MkChart.vue
index fe7077bdbf..adb3c134ae 100644
--- a/packages/frontend/src/components/MkChart.vue
+++ b/packages/frontend/src/components/MkChart.vue
@@ -74,7 +74,7 @@ const props = defineProps({
},
});
-let legendEl = $shallowRef<InstanceType<typeof MkChartLegend>>();
+const legendEl = shallowRef<InstanceType<typeof MkChartLegend>>();
const sum = (...arr) => arr.reduce((r, a) => r.map((b, i) => a[i] + b));
const negate = arr => arr.map(x => -x);
@@ -268,7 +268,7 @@ const render = () => {
gradient,
},
},
- plugins: [chartVLine(vLineColor), ...(props.detailed ? [chartLegend(legendEl)] : [])],
+ plugins: [chartVLine(vLineColor), ...(props.detailed ? [chartLegend(legendEl.value)] : [])],
});
};
diff --git a/packages/frontend/src/components/MkChartLegend.vue b/packages/frontend/src/components/MkChartLegend.vue
index 546bc0b4b1..c265fe6e97 100644
--- a/packages/frontend/src/components/MkChartLegend.vue
+++ b/packages/frontend/src/components/MkChartLegend.vue
@@ -13,29 +13,30 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
+import { shallowRef } from 'vue';
import { Chart, LegendItem } from 'chart.js';
const props = defineProps({
});
-let chart = $shallowRef<Chart>();
-let items = $shallowRef<LegendItem[]>([]);
+const chart = shallowRef<Chart>();
+const items = shallowRef<LegendItem[]>([]);
function update(_chart: Chart, _items: LegendItem[]) {
- chart = _chart,
- items = _items;
+ chart.value = _chart,
+ items.value = _items;
}
function onClick(item: LegendItem) {
- if (chart == null) return;
- const { type } = chart.config;
+ if (chart.value == null) return;
+ const { type } = chart.value.config;
if (type === 'pie' || type === 'doughnut') {
// Pie and doughnut charts only have a single dataset and visibility is per item
- chart.toggleDataVisibility(item.index);
+ chart.value.toggleDataVisibility(item.index);
} else {
- chart.setDatasetVisibility(item.datasetIndex, !chart.isDatasetVisible(item.datasetIndex));
+ chart.value.setDatasetVisibility(item.datasetIndex, !chart.value.isDatasetVisible(item.datasetIndex));
}
- chart.update();
+ chart.value.update();
}
defineExpose({
diff --git a/packages/frontend/src/components/MkClickerGame.vue b/packages/frontend/src/components/MkClickerGame.vue
index 71914c6886..1e72319010 100644
--- a/packages/frontend/src/components/MkClickerGame.vue
+++ b/packages/frontend/src/components/MkClickerGame.vue
@@ -19,7 +19,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { computed, onMounted, onUnmounted } from 'vue';
+import { computed, onMounted, onUnmounted, ref } from 'vue';
import MkPlusOneEffect from '@/components/MkPlusOneEffect.vue';
import * as os from '@/os.js';
import { useInterval } from '@/scripts/use-interval.js';
@@ -29,8 +29,8 @@ import { claimAchievement } from '@/scripts/achievements.js';
const saveData = game.saveData;
const cookies = computed(() => saveData.value?.cookies);
-let cps = $ref(0);
-let prevCookies = $ref(0);
+const cps = ref(0);
+const prevCookies = ref(0);
function onClick(ev: MouseEvent) {
const x = ev.clientX;
@@ -48,9 +48,9 @@ function onClick(ev: MouseEvent) {
}
useInterval(() => {
- const diff = saveData.value!.cookies - prevCookies;
- cps = diff;
- prevCookies = saveData.value!.cookies;
+ const diff = saveData.value!.cookies - prevCookies.value;
+ cps.value = diff;
+ prevCookies.value = saveData.value!.cookies;
}, 1000, {
immediate: false,
afterMounted: true,
@@ -63,7 +63,7 @@ useInterval(game.save, 1000 * 5, {
onMounted(async () => {
await game.load();
- prevCookies = saveData.value!.cookies;
+ prevCookies.value = saveData.value!.cookies;
});
onUnmounted(() => {
diff --git a/packages/frontend/src/components/MkCode.core.vue b/packages/frontend/src/components/MkCode.core.vue
index 21684b462a..19418cd4da 100644
--- a/packages/frontend/src/components/MkCode.core.vue
+++ b/packages/frontend/src/components/MkCode.core.vue
@@ -54,7 +54,7 @@ watch(() => props.lang, (to) => {
return new Promise((resolve) => {
fetchLanguage(to).then(() => resolve);
});
-}, { immediate: true, });
+}, { immediate: true });
</script>
<style scoped lang="scss">
@@ -62,7 +62,7 @@ watch(() => props.lang, (to) => {
padding: 1em;
margin: .5em 0;
overflow: auto;
- border-radius: .3em;
+ border-radius: 8px;
& pre,
& code {
diff --git a/packages/frontend/src/components/MkCode.vue b/packages/frontend/src/components/MkCode.vue
index b39e6ff23c..2c016e4d7c 100644
--- a/packages/frontend/src/components/MkCode.vue
+++ b/packages/frontend/src/components/MkCode.vue
@@ -4,18 +4,27 @@ SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
- <Suspense>
- <template #fallback>
- <MkLoading v-if="!inline ?? true" />
- </template>
- <code v-if="inline" :class="$style.codeInlineRoot">{{ code }}</code>
- <XCode v-else :code="code" :lang="lang"/>
- </Suspense>
+<Suspense>
+ <template #fallback>
+ <MkLoading v-if="!inline ?? true"/>
+ </template>
+ <code v-if="inline" :class="$style.codeInlineRoot">{{ code }}</code>
+ <XCode v-else-if="show && lang" :code="code" :lang="lang"/>
+ <pre v-else-if="show" :class="$style.codeBlockFallbackRoot"><code :class="$style.codeBlockFallbackCode">{{ code }}</code></pre>
+ <button v-else :class="$style.codePlaceholderRoot" @click="show = true">
+ <div :class="$style.codePlaceholderContainer">
+ <div><i class="ti ti-code"></i> {{ i18n.ts.code }}</div>
+ <div>{{ i18n.ts.clickToShow }}</div>
+ </div>
+ </button>
+</Suspense>
</template>
<script lang="ts" setup>
-import { defineAsyncComponent } from 'vue';
+import { defineAsyncComponent, ref } from 'vue';
import MkLoading from '@/components/global/MkLoading.vue';
+import { defaultStore } from '@/store.js';
+import { i18n } from '@/i18n.js';
defineProps<{
code: string;
@@ -23,6 +32,8 @@ defineProps<{
inline?: boolean;
}>();
+const show = ref(!defaultStore.state.dataSaver.code);
+
const XCode = defineAsyncComponent(() => import('@/components/MkCode.core.vue'));
</script>
@@ -36,4 +47,42 @@ const XCode = defineAsyncComponent(() => import('@/components/MkCode.core.vue'))
padding: .1em;
border-radius: .3em;
}
+
+.codeBlockFallbackRoot {
+ display: block;
+ overflow-wrap: anywhere;
+ color: #D4D4D4;
+ background: #1E1E1E;
+ padding: 1em;
+ margin: .5em 0;
+ overflow: auto;
+ border-radius: 8px;
+}
+
+.codeBlockFallbackCode {
+ font-family: Consolas, Monaco, Andale Mono, Ubuntu Mono, monospace;
+}
+
+.codePlaceholderRoot {
+ display: block;
+ width: 100%;
+ background: none;
+ border: none;
+ outline: none;
+ font: inherit;
+ color: inherit;
+ cursor: pointer;
+
+ box-sizing: border-box;
+ border-radius: 8px;
+ padding: 24px;
+ margin-top: 4px;
+ color: #D4D4D4;
+ background: #1E1E1E;
+}
+
+.codePlaceholderContainer {
+ text-align: center;
+ font-size: 0.8em;
+}
</style>
diff --git a/packages/frontend/src/components/MkCodeEditor.vue b/packages/frontend/src/components/MkCodeEditor.vue
index 5434042684..c9bcc71196 100644
--- a/packages/frontend/src/components/MkCodeEditor.vue
+++ b/packages/frontend/src/components/MkCodeEditor.vue
@@ -4,30 +4,38 @@ SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
-<div :class="[$style.codeEditorRoot, { [$style.disabled]: disabled, [$style.focused]: focused }]">
- <div :class="$style.codeEditorScroller">
- <textarea
- ref="inputEl"
- v-model="vModel"
- :class="[$style.textarea]"
- :disabled="disabled"
- :required="required"
- :readonly="readonly"
- autocomplete="off"
- wrap="off"
- spellcheck="false"
- @focus="focused = true"
- @blur="focused = false"
- @keydown="onKeydown($event)"
- @input="onInput"
- ></textarea>
- <XCode :class="$style.codeEditorHighlighter" :codeEditor="true" :code="v" :lang="lang"/>
+<div>
+ <div :class="$style.label" @click="focus"><slot name="label"></slot></div>
+ <div :class="[$style.codeEditorRoot, { [$style.focused]: focused }]">
+ <div :class="$style.codeEditorScroller">
+ <textarea
+ ref="inputEl"
+ v-model="vModel"
+ :class="[$style.textarea]"
+ :disabled="disabled"
+ :required="required"
+ :readonly="readonly"
+ autocomplete="off"
+ wrap="off"
+ spellcheck="false"
+ @focus="focused = true"
+ @blur="focused = false"
+ @keydown="onKeydown($event)"
+ @input="onInput"
+ ></textarea>
+ <XCode :class="$style.codeEditorHighlighter" :codeEditor="true" :code="v" :lang="lang"/>
+ </div>
</div>
+ <div :class="$style.caption"><slot name="caption"></slot></div>
+ <MkButton v-if="manualSave && changed" primary :class="$style.save" @click="updated"><i class="ti ti-device-floppy"></i> {{ i18n.ts.save }}</MkButton>
</div>
</template>
<script lang="ts" setup>
import { ref, watch, toRefs, shallowRef, nextTick } from 'vue';
+import { debounce } from 'throttle-debounce';
+import MkButton from '@/components/MkButton.vue';
+import { i18n } from '@/i18n.js';
import XCode from '@/components/MkCode.core.vue';
const props = withDefaults(defineProps<{
@@ -36,6 +44,8 @@ const props = withDefaults(defineProps<{
required?: boolean;
readonly?: boolean;
disabled?: boolean;
+ debounce?: boolean;
+ manualSave?: boolean;
}>(), {
lang: 'js',
});
@@ -54,6 +64,8 @@ const focused = ref(false);
const changed = ref(false);
const inputEl = shallowRef<HTMLTextAreaElement>();
+const focus = () => inputEl.value?.focus();
+
const onInput = (ev) => {
v.value = ev.target?.value ?? v.value;
changed.value = true;
@@ -100,16 +112,48 @@ const updated = () => {
emit('update:modelValue', v.value);
};
+const debouncedUpdated = debounce(1000, updated);
+
watch(modelValue, newValue => {
v.value = newValue ?? '';
});
-watch(v, () => {
- updated();
+watch(v, newValue => {
+ if (!props.manualSave) {
+ if (props.debounce) {
+ debouncedUpdated();
+ } else {
+ updated();
+ }
+ }
});
</script>
<style lang="scss" module>
+.label {
+ font-size: 0.85em;
+ padding: 0 0 8px 0;
+ user-select: none;
+
+ &:empty {
+ display: none;
+ }
+}
+
+.caption {
+ font-size: 0.85em;
+ padding: 8px 0 0 0;
+ color: var(--fgTransparentWeak);
+
+ &:empty {
+ display: none;
+ }
+}
+
+.save {
+ margin: 8px 0 0 0;
+}
+
.codeEditorRoot {
min-width: 100%;
max-width: 100%;
@@ -117,6 +161,7 @@ watch(v, () => {
overflow-y: hidden;
box-sizing: border-box;
margin: 0;
+ border-radius: 6px;
padding: 0;
color: var(--fg);
border: solid 1px var(--panel);
@@ -139,6 +184,10 @@ watch(v, () => {
height: 100%;
}
+.textarea, .codeEditorHighlighter {
+ margin: 0;
+}
+
.textarea {
position: absolute;
top: 0;
@@ -153,7 +202,10 @@ watch(v, () => {
caret-color: rgb(225, 228, 232);
background-color: transparent;
border: 0;
+ border-radius: 6px;
outline: 0;
+ min-width: calc(100% - 24px);
+ height: 100%;
padding: 12px;
line-height: 1.5em;
font-size: 1em;
diff --git a/packages/frontend/src/components/MkColorInput.vue b/packages/frontend/src/components/MkColorInput.vue
index 79b1949640..4f15e88951 100644
--- a/packages/frontend/src/components/MkColorInput.vue
+++ b/packages/frontend/src/components/MkColorInput.vue
@@ -24,8 +24,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { onMounted, nextTick, ref, shallowRef, watch, computed, toRefs } from 'vue';
-import { i18n } from '@/i18n.js';
+import { ref, shallowRef, toRefs } from 'vue';
const props = defineProps<{
modelValue: string | null;
diff --git a/packages/frontend/src/components/MkContextMenu.vue b/packages/frontend/src/components/MkContextMenu.vue
index 6cca7fc353..b78252be89 100644
--- a/packages/frontend/src/components/MkContextMenu.vue
+++ b/packages/frontend/src/components/MkContextMenu.vue
@@ -18,7 +18,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { onMounted, onBeforeUnmount } from 'vue';
+import { onMounted, onBeforeUnmount, shallowRef, ref } from 'vue';
import MkMenu from './MkMenu.vue';
import { MenuItem } from './types/menu.vue';
import contains from '@/scripts/contains.js';
@@ -34,9 +34,9 @@ const emit = defineEmits<{
(ev: 'closed'): void;
}>();
-let rootEl = $shallowRef<HTMLDivElement>();
+const rootEl = shallowRef<HTMLDivElement>();
-let zIndex = $ref<number>(os.claimZIndex('high'));
+const zIndex = ref<number>(os.claimZIndex('high'));
const SCROLLBAR_THICKNESS = 16;
@@ -44,8 +44,8 @@ onMounted(() => {
let left = props.ev.pageX + 1; // 間違って右ダブルクリックした場合に意図せずアイテムがクリックされるのを防ぐため + 1
let top = props.ev.pageY + 1; // 間違って右ダブルクリックした場合に意図せずアイテムがクリックされるのを防ぐため + 1
- const width = rootEl.offsetWidth;
- const height = rootEl.offsetHeight;
+ const width = rootEl.value.offsetWidth;
+ const height = rootEl.value.offsetHeight;
if (left + width - window.pageXOffset >= (window.innerWidth - SCROLLBAR_THICKNESS)) {
left = (window.innerWidth - SCROLLBAR_THICKNESS) - width + window.pageXOffset;
@@ -63,8 +63,8 @@ onMounted(() => {
left = 0;
}
- rootEl.style.top = `${top}px`;
- rootEl.style.left = `${left}px`;
+ rootEl.value.style.top = `${top}px`;
+ rootEl.value.style.left = `${left}px`;
document.body.addEventListener('mousedown', onMousedown);
});
@@ -74,7 +74,7 @@ onBeforeUnmount(() => {
});
function onMousedown(evt: Event) {
- if (!contains(rootEl, evt.target) && (rootEl !== evt.target)) emit('closed');
+ if (!contains(rootEl.value, evt.target) && (rootEl.value !== evt.target)) emit('closed');
}
</script>
diff --git a/packages/frontend/src/components/MkCropperDialog.vue b/packages/frontend/src/components/MkCropperDialog.vue
index 81f3936600..0a1ddd3171 100644
--- a/packages/frontend/src/components/MkCropperDialog.vue
+++ b/packages/frontend/src/components/MkCropperDialog.vue
@@ -31,7 +31,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { onMounted } from 'vue';
+import { onMounted, shallowRef, ref } from 'vue';
import * as Misskey from 'misskey-js';
import Cropper from 'cropperjs';
import tinycolor from 'tinycolor2';
@@ -56,10 +56,10 @@ const props = defineProps<{
}>();
const imgUrl = getProxiedImageUrl(props.file.url, undefined, true);
-let dialogEl = $shallowRef<InstanceType<typeof MkModalWindow>>();
-let imgEl = $shallowRef<HTMLImageElement>();
+const dialogEl = shallowRef<InstanceType<typeof MkModalWindow>>();
+const imgEl = shallowRef<HTMLImageElement>();
let cropper: Cropper | null = null;
-let loading = $ref(true);
+const loading = ref(true);
const ok = async () => {
const promise = new Promise<Misskey.entities.DriveFile>(async (res) => {
@@ -94,16 +94,16 @@ const ok = async () => {
const f = await promise;
emit('ok', f);
- dialogEl!.close();
+ dialogEl.value!.close();
};
const cancel = () => {
emit('cancel');
- dialogEl!.close();
+ dialogEl.value!.close();
};
const onImageLoad = () => {
- loading = false;
+ loading.value = false;
if (cropper) {
cropper.getCropperImage()!.$center('contain');
@@ -112,7 +112,7 @@ const onImageLoad = () => {
};
onMounted(() => {
- cropper = new Cropper(imgEl!, {
+ cropper = new Cropper(imgEl.value!, {
});
const computedStyle = getComputedStyle(document.documentElement);
diff --git a/packages/frontend/src/components/MkCwButton.vue b/packages/frontend/src/components/MkCwButton.vue
index 0cdaf7c9bd..4a6d2dfba2 100644
--- a/packages/frontend/src/components/MkCwButton.vue
+++ b/packages/frontend/src/components/MkCwButton.vue
@@ -16,7 +16,23 @@ import MkButton from '@/components/MkButton.vue';
const props = defineProps<{
modelValue: boolean;
- note: Misskey.entities.Note;
+ text: string | null;
+ renote: Misskey.entities.Note | null;
+ files: Misskey.entities.DriveFile[];
+ poll?: {
+ expiresAt: string | null;
+ multiple: boolean;
+ choices: {
+ isVoted: boolean;
+ text: string;
+ votes: number;
+ }[];
+ } | {
+ choices: string[];
+ multiple: boolean;
+ expiresAt: string | null;
+ expiredAfter: string | null;
+ };
}>();
const emit = defineEmits<{
@@ -25,9 +41,10 @@ const emit = defineEmits<{
const label = computed(() => {
return concat([
- props.note.text ? [i18n.t('_cw.chars', { count: props.note.text.length })] : [],
- props.note.files && props.note.files.length !== 0 ? [i18n.t('_cw.files', { count: props.note.files.length })] : [],
- props.note.poll != null ? [i18n.ts.poll] : [],
+ props.text ? [i18n.t('_cw.chars', { count: props.text.length })] : [],
+ props.renote ? [i18n.ts.quote] : [],
+ props.files.length !== 0 ? [i18n.t('_cw.files', { count: props.files.length })] : [],
+ props.poll != null ? [i18n.ts.poll] : [],
] as string[][]).join(' / ');
});
diff --git a/packages/frontend/src/components/MkDialog.vue b/packages/frontend/src/components/MkDialog.vue
index e0692eb383..2c0f6a4d78 100644
--- a/packages/frontend/src/components/MkDialog.vue
+++ b/packages/frontend/src/components/MkDialog.vue
@@ -30,8 +30,8 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkInput v-if="input" v-model="inputValue" autofocus :type="input.type || 'text'" :placeholder="input.placeholder || undefined" :autocomplete="input.autocomplete" @keydown="onInputKeydown">
<template v-if="input.type === 'password'" #prefix><i class="ph-lock ph-bold ph-lg"></i></template>
<template #caption>
- <span v-if="okButtonDisabled && disabledReason === 'charactersExceeded'" v-text="i18n.t('_dialog.charactersExceeded', { current: (inputValue as string).length, max: input.maxLength ?? 'NaN' })"/>
- <span v-else-if="okButtonDisabled && disabledReason === 'charactersBelow'" v-text="i18n.t('_dialog.charactersBelow', { current: (inputValue as string).length, min: input.minLength ?? 'NaN' })"/>
+ <span v-if="okButtonDisabledReason === 'charactersExceeded'" v-text="i18n.t('_dialog.charactersExceeded', { current: (inputValue as string).length, max: input.maxLength ?? 'NaN' })"/>
+ <span v-else-if="okButtonDisabledReason === 'charactersBelow'" v-text="i18n.t('_dialog.charactersBelow', { current: (inputValue as string).length, min: input.minLength ?? 'NaN' })"/>
</template>
</MkInput>
<MkSelect v-if="select" v-model="selectedValue" autofocus>
@@ -45,7 +45,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
</MkSelect>
<div v-if="(showOkButton || showCancelButton) && !actions" :class="$style.buttons">
- <MkButton v-if="showOkButton" data-cy-modal-dialog-ok inline primary rounded :autofocus="!input && !select" :disabled="okButtonDisabled" @click="ok">{{ okText ?? ((showCancelButton || input || select) ? i18n.ts.ok : i18n.ts.gotIt) }}</MkButton>
+ <MkButton v-if="showOkButton" data-cy-modal-dialog-ok inline primary rounded :autofocus="!input && !select" :disabled="okButtonDisabledReason" @click="ok">{{ okText ?? ((showCancelButton || input || select) ? i18n.ts.ok : i18n.ts.gotIt) }}</MkButton>
<MkButton v-if="showCancelButton || input || select" data-cy-modal-dialog-cancel inline rounded @click="cancel">{{ cancelText ?? i18n.ts.cancel }}</MkButton>
</div>
<div v-if="actions" :class="$style.buttons">
@@ -56,7 +56,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { onBeforeUnmount, onMounted, ref, shallowRef } from 'vue';
+import { onBeforeUnmount, onMounted, ref, shallowRef, computed } from 'vue';
import MkModal from '@/components/MkModal.vue';
import MkButton from '@/components/MkButton.vue';
import MkInput from '@/components/MkInput.vue';
@@ -122,24 +122,21 @@ const modal = shallowRef<InstanceType<typeof MkModal>>();
const inputValue = ref<string | number | null>(props.input?.default ?? null);
const selectedValue = ref(props.select?.default ?? null);
-let disabledReason = $ref<null | 'charactersExceeded' | 'charactersBelow'>(null);
-const okButtonDisabled = $computed<boolean>(() => {
+const okButtonDisabledReason = computed<null | 'charactersExceeded' | 'charactersBelow'>(() => {
if (props.input) {
if (props.input.minLength) {
if ((inputValue.value || inputValue.value === '') && (inputValue.value as string).length < props.input.minLength) {
- disabledReason = 'charactersBelow';
- return true;
+ return 'charactersBelow';
}
}
if (props.input.maxLength) {
if (inputValue.value && (inputValue.value as string).length > props.input.maxLength) {
- disabledReason = 'charactersExceeded';
- return true;
+ return 'charactersExceeded';
}
}
}
- return false;
+ return null;
});
function done(canceled: boolean, result?) {
diff --git a/packages/frontend/src/components/MkDrive.folder.vue b/packages/frontend/src/components/MkDrive.folder.vue
index 9f79a44d4c..dcaaa72cf4 100644
--- a/packages/frontend/src/components/MkDrive.folder.vue
+++ b/packages/frontend/src/components/MkDrive.folder.vue
@@ -39,6 +39,7 @@ import { i18n } from '@/i18n.js';
import { defaultStore } from '@/store.js';
import { claimAchievement } from '@/scripts/achievements.js';
import copyToClipboard from '@/scripts/copy-to-clipboard.js';
+import { MenuItem } from '@/types/menu.js';
const props = withDefaults(defineProps<{
folder: Misskey.entities.DriveFolder;
@@ -250,7 +251,7 @@ function setAsUploadFolder() {
}
function onContextmenu(ev: MouseEvent) {
- let menu;
+ let menu: MenuItem[];
menu = [{
text: i18n.ts.openInWindow,
icon: 'ph-app-window ph-bold ph-lg',
@@ -260,18 +261,18 @@ function onContextmenu(ev: MouseEvent) {
}, {
}, 'closed');
},
- }, null, {
+ }, { type: 'divider' }, {
text: i18n.ts.rename,
icon: 'ph-textbox ph-bold ph-lg',
action: rename,
- }, null, {
+ }, { type: 'divider' }, {
text: i18n.ts.delete,
icon: 'ph-trash ph-bold ph-lg',
danger: true,
action: deleteFolder,
}];
if (defaultStore.state.devMode) {
- menu = menu.concat([null, {
+ menu = menu.concat([{ type: 'divider' }, {
icon: 'ph-identification-card ph-bold ph-lg',
text: i18n.ts.copyFolderId,
action: () => {
diff --git a/packages/frontend/src/components/MkDrive.vue b/packages/frontend/src/components/MkDrive.vue
index 5281541927..00bb0e6e2b 100644
--- a/packages/frontend/src/components/MkDrive.vue
+++ b/packages/frontend/src/components/MkDrive.vue
@@ -616,7 +616,7 @@ function getMenu() {
type: 'switch',
text: i18n.ts.keepOriginalUploading,
ref: keepOriginal,
- }, null, {
+ }, { type: 'divider' }, {
text: i18n.ts.addFile,
type: 'label',
}, {
@@ -627,7 +627,7 @@ function getMenu() {
text: i18n.ts.fromUrl,
icon: 'ph-link ph-bold ph-lg',
action: () => { urlUpload(); },
- }, null, {
+ }, { type: 'divider' }, {
text: folder.value ? folder.value.name : i18n.ts.drive,
type: 'label',
}, folder.value ? {
diff --git a/packages/frontend/src/components/MkEmojiPicker.section.vue b/packages/frontend/src/components/MkEmojiPicker.section.vue
index 00e0a0e042..49c146b68d 100644
--- a/packages/frontend/src/components/MkEmojiPicker.section.vue
+++ b/packages/frontend/src/components/MkEmojiPicker.section.vue
@@ -5,9 +5,10 @@ SPDX-License-Identifier: AGPL-3.0-only
<template>
<!-- このコンポーネントの要素のclassは親から利用されるのでむやみに弄らないこと -->
-<section>
+<!-- フォルダの中にはカスタム絵文字だけ(Unicode絵文字もこっち) -->
+<section v-if="!hasChildSection" v-panel style="border-radius: 6px; border-bottom: 0.5px solid var(--divider);">
<header class="_acrylic" @click="shown = !shown">
- <i class="toggle ti-fw" :class="shown ? 'ph-caret-down ph-bold ph-lg' : 'ph-caret-up ph-bold ph-lg'"></i> <slot></slot> ({{ emojis.length }})
+ <i class="toggle ti-fw" :class="shown ? 'ph-caret-down ph-bold ph-lg' : 'ph-caret-up ph-bold ph-lg'"></i> <slot></slot> (<i class="ph-bold ph-lg"></i>:{{ emojis.length }})
</header>
<div v-if="shown" class="body">
<button
@@ -23,15 +24,52 @@ SPDX-License-Identifier: AGPL-3.0-only
</button>
</div>
</section>
+<!-- フォルダの中にはカスタム絵文字やフォルダがある -->
+<section v-else v-panel style="border-radius: 6px; border-bottom: 0.5px solid var(--divider);">
+ <header class="_acrylic" @click="shown = !shown">
+ <i class="toggle ti-fw" :class="shown ? 'ph-caret-down ph-bold ph-lg' : 'ph-caret-up ph-bold ph-lg'"></i> <slot></slot> (<i class="ph-folder ph-bold ph-lg"></i>:{{ customEmojiTree.length }} <i class="ti ti-icons ti-fw"></i>:{{ emojis.length }})
+ </header>
+ <div v-if="shown" style="padding-left: 9px;">
+ <MkEmojiPickerSection
+ v-for="child in customEmojiTree"
+ :key="`custom:${child.value}`"
+ :initialShown="initialShown"
+ :emojis="computed(() => customEmojis.filter(e => e.category === child.category).map(e => `:${e.name}:`))"
+ :hasChildSection="child.children.length !== 0"
+ :customEmojiTree="child.children"
+ @chosen="nestedChosen"
+ >
+ {{ child.value || i18n.ts.other }}
+ </MkEmojiPickerSection>
+ </div>
+ <div v-if="shown" class="body">
+ <button
+ v-for="emoji in emojis"
+ :key="emoji"
+ :data-emoji="emoji"
+ class="_button item"
+ @pointerenter="computeButtonTitle"
+ @click="emit('chosen', emoji, $event)"
+ >
+ <MkCustomEmoji v-if="emoji[0] === ':'" class="emoji" :name="emoji" :normal="true"/>
+ <MkEmoji v-else class="emoji" :emoji="emoji" :normal="true"/>
+ </button>
+ </div>
+</section>
</template>
<script lang="ts" setup>
import { ref, computed, Ref } from 'vue';
-import { getEmojiName } from '@/scripts/emojilist.js';
+import { i18n } from '../i18n.js';
+import { CustomEmojiFolderTree, getEmojiName } from '@/scripts/emojilist.js';
+import { customEmojis } from '@/custom-emojis.js';
+import MkEmojiPickerSection from '@/components/MkEmojiPicker.section.vue';
const props = defineProps<{
emojis: string[] | Ref<string[]>;
initialShown?: boolean;
+ hasChildSection?: boolean;
+ customEmojiTree?: CustomEmojiFolderTree[];
}>();
const emit = defineEmits<{
@@ -49,4 +87,7 @@ function computeButtonTitle(ev: MouseEvent): void {
elm.title = getEmojiName(emoji) ?? emoji;
}
+function nestedChosen(emoji: any, ev?: MouseEvent) {
+ emit('chosen', emoji, ev);
+}
</script>
diff --git a/packages/frontend/src/components/MkEmojiPicker.vue b/packages/frontend/src/components/MkEmojiPicker.vue
index 50ed8048bb..b7e329d7c2 100644
--- a/packages/frontend/src/components/MkEmojiPicker.vue
+++ b/packages/frontend/src/components/MkEmojiPicker.vue
@@ -36,7 +36,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</section>
<div v-if="tab === 'index'" class="group index">
- <section v-if="showPinned">
+ <section v-if="showPinned && pinned.length > 0">
<div class="body">
<button
v-for="emoji in pinned"
@@ -73,18 +73,20 @@ SPDX-License-Identifier: AGPL-3.0-only
<div v-once class="group">
<header class="_acrylic">{{ i18n.ts.customEmojis }}</header>
<XSection
- v-for="category in customEmojiCategories"
- :key="`custom:${category}`"
+ v-for="child in customEmojiFolderRoot.children"
+ :key="`custom:${child.value}`"
:initialShown="false"
- :emojis="computed(() => customEmojis.filter(e => category === null ? (e.category === 'null' || !e.category) : e.category === category).filter(filterAvailable).map(e => `:${e.name}:`))"
+ :emojis="computed(() => customEmojis.filter(e => child.value === '' ? (e.category === 'null' || !e.category) : e.category === child.value).filter(filterAvailable).map(e => `:${e.name}:`))"
+ :hasChildSection="child.children.length !== 0"
+ :customEmojiTree="child.children"
@chosen="chosen"
>
- {{ category || i18n.ts.other }}
+ {{ child.value || i18n.ts.other }}
</XSection>
</div>
<div v-once class="group">
<header class="_acrylic">{{ i18n.ts.emoji }}</header>
- <XSection v-for="category in categories" :key="category" :emojis="emojiCharByCategory.get(category) ?? []" @chosen="chosen">{{ category }}</XSection>
+ <XSection v-for="category in categories" :key="category" :emojis="emojiCharByCategory.get(category) ?? []" :hasChildSection="false" @chosen="chosen">{{ category }}</XSection>
</div>
</div>
<div class="tabs">
@@ -100,7 +102,14 @@ SPDX-License-Identifier: AGPL-3.0-only
import { ref, shallowRef, computed, watch, onMounted } from 'vue';
import * as Misskey from 'misskey-js';
import XSection from '@/components/MkEmojiPicker.section.vue';
-import { emojilist, emojiCharByCategory, UnicodeEmojiDef, unicodeEmojiCategories as categories, getEmojiName } from '@/scripts/emojilist.js';
+import {
+ emojilist,
+ emojiCharByCategory,
+ UnicodeEmojiDef,
+ unicodeEmojiCategories as categories,
+ getEmojiName,
+ CustomEmojiFolderTree,
+} from '@/scripts/emojilist.js';
import MkRippleEffect from '@/components/MkRippleEffect.vue';
import * as os from '@/os.js';
import { isTouchUsing } from '@/scripts/touch.js';
@@ -112,10 +121,11 @@ import { $i } from '@/account.js';
const props = withDefaults(defineProps<{
showPinned?: boolean;
- asReactionPicker?: boolean;
+ pinnedEmojis?: string[];
maxHeight?: number;
asDrawer?: boolean;
asWindow?: boolean;
+ asReactionPicker?: boolean; // 今は使われてないが将来的に使いそう
}>(), {
showPinned: true,
});
@@ -128,22 +138,50 @@ const searchEl = shallowRef<HTMLInputElement>();
const emojisEl = shallowRef<HTMLDivElement>();
const {
- reactions: pinned,
- reactionPickerSize,
- reactionPickerWidth,
- reactionPickerHeight,
- disableShowingAnimatedImages,
+ emojiPickerScale,
+ emojiPickerWidth,
+ emojiPickerHeight,
recentlyUsedEmojis,
} = defaultStore.reactiveState;
-const size = computed(() => props.asReactionPicker ? reactionPickerSize.value : 1);
-const width = computed(() => props.asReactionPicker ? reactionPickerWidth.value : 3);
-const height = computed(() => props.asReactionPicker ? reactionPickerHeight.value : 2);
+const pinned = computed(() => props.pinnedEmojis);
+const size = computed(() => emojiPickerScale.value);
+const width = computed(() => emojiPickerWidth.value);
+const height = computed(() => emojiPickerHeight.value);
const q = ref<string>('');
-const searchResultCustom = ref<Misskey.entities.CustomEmoji[]>([]);
+const searchResultCustom = ref<Misskey.entities.EmojiSimple[]>([]);
const searchResultUnicode = ref<UnicodeEmojiDef[]>([]);
const tab = ref<'index' | 'custom' | 'unicode' | 'tags'>('index');
+const customEmojiFolderRoot: CustomEmojiFolderTree = { value: '', category: '', children: [] };
+
+function parseAndMergeCategories(input: string, root: CustomEmojiFolderTree): CustomEmojiFolderTree {
+ const parts = input.split('/').map(p => p.trim());
+ let currentNode: CustomEmojiFolderTree = root;
+
+ for (const part of parts) {
+ let existingNode = currentNode.children.find((node) => node.value === part);
+
+ if (!existingNode) {
+ const newNode: CustomEmojiFolderTree = { value: part, category: input, children: [] };
+ currentNode.children.push(newNode);
+ existingNode = newNode;
+ }
+
+ currentNode = existingNode;
+ }
+
+ return currentNode;
+}
+
+customEmojiCategories.value.forEach(ec => {
+ if (ec !== null) {
+ parseAndMergeCategories(ec, customEmojiFolderRoot);
+ }
+});
+
+parseAndMergeCategories('', customEmojiFolderRoot);
+
watch(q, () => {
if (emojisEl.value) emojisEl.value.scrollTop = 0;
@@ -158,7 +196,7 @@ watch(q, () => {
const searchCustom = () => {
const max = 100;
const emojis = customEmojis.value;
- const matches = new Set<Misskey.entities.CustomEmoji>();
+ const matches = new Set<Misskey.entities.EmojiSimple>();
const exactMatch = emojis.find(emoji => emoji.name === newQ);
if (exactMatch) matches.add(exactMatch);
@@ -288,7 +326,7 @@ watch(q, () => {
searchResultUnicode.value = Array.from(searchUnicode());
});
-function filterAvailable(emoji: Misskey.entities.CustomEmoji): boolean {
+function filterAvailable(emoji: Misskey.entities.EmojiSimple): boolean {
return (emoji.roleIdsThatCanBeUsedThisEmojiAsReaction == null || emoji.roleIdsThatCanBeUsedThisEmojiAsReaction.length === 0) || ($i && $i.roles.some(r => emoji.roleIdsThatCanBeUsedThisEmojiAsReaction.includes(r.id)));
}
@@ -305,7 +343,7 @@ function reset() {
q.value = '';
}
-function getKey(emoji: string | Misskey.entities.CustomEmoji | UnicodeEmojiDef): string {
+function getKey(emoji: string | Misskey.entities.EmojiSimple | UnicodeEmojiDef): string {
return typeof emoji === 'string' ? emoji : 'char' in emoji ? emoji.char : `:${emoji.name}:`;
}
@@ -329,7 +367,7 @@ function chosen(emoji: any, ev?: MouseEvent) {
emit('chosen', key);
// 最近使った絵文字更新
- if (!pinned.value.includes(key)) {
+ if (!pinned.value?.includes(key)) {
let recents = defaultStore.state.recentlyUsedEmojis;
recents = recents.filter((emoji: any) => emoji !== key);
recents.unshift(key);
@@ -572,8 +610,7 @@ defineExpose({
position: sticky;
top: 0;
left: 0;
- height: 32px;
- line-height: 32px;
+ line-height: 28px;
z-index: 1;
padding: 0 8px;
font-size: 12px;
diff --git a/packages/frontend/src/components/MkEmojiPickerDialog.vue b/packages/frontend/src/components/MkEmojiPickerDialog.vue
index 581d815d66..4068a79f08 100644
--- a/packages/frontend/src/components/MkEmojiPickerDialog.vue
+++ b/packages/frontend/src/components/MkEmojiPickerDialog.vue
@@ -8,7 +8,7 @@ SPDX-License-Identifier: AGPL-3.0-only
ref="modal"
v-slot="{ type, maxHeight }"
:zPriority="'middle'"
- :preferType="asReactionPicker && defaultStore.state.reactionPickerUseDrawerForMobile === false ? 'popup' : 'auto'"
+ :preferType="defaultStore.state.emojiPickerUseDrawerForMobile === false ? 'popup' : 'auto'"
:transparentBg="true"
:manualShowing="manualShowing"
:src="src"
@@ -22,6 +22,7 @@ SPDX-License-Identifier: AGPL-3.0-only
class="_popup _shadow"
:class="{ [$style.drawer]: type === 'drawer' }"
:showPinned="showPinned"
+ :pinnedEmojis="pinnedEmojis"
:asReactionPicker="asReactionPicker"
:asDrawer="type === 'drawer'"
:max-height="maxHeight"
@@ -36,15 +37,19 @@ import MkModal from '@/components/MkModal.vue';
import MkEmojiPicker from '@/components/MkEmojiPicker.vue';
import { defaultStore } from '@/store.js';
-withDefaults(defineProps<{
+const props = withDefaults(defineProps<{
manualShowing?: boolean | null;
src?: HTMLElement;
showPinned?: boolean;
+ pinnedEmojis?: string[],
asReactionPicker?: boolean;
+ choseAndClose?: boolean;
}>(), {
manualShowing: null,
showPinned: true,
+ pinnedEmojis: undefined,
asReactionPicker: false,
+ choseAndClose: true,
});
const emit = defineEmits<{
@@ -58,7 +63,9 @@ const picker = shallowRef<InstanceType<typeof MkEmojiPicker>>();
function chosen(emoji: any) {
emit('done', emoji);
- modal.value?.close();
+ if (props.choseAndClose) {
+ modal.value?.close();
+ }
}
function opening() {
diff --git a/packages/frontend/src/components/MkFeaturedPhotos.vue b/packages/frontend/src/components/MkFeaturedPhotos.vue
index cef1943d5c..6d1bad7433 100644
--- a/packages/frontend/src/components/MkFeaturedPhotos.vue
+++ b/packages/frontend/src/components/MkFeaturedPhotos.vue
@@ -12,7 +12,7 @@ import { ref } from 'vue';
import * as Misskey from 'misskey-js';
import * as os from '@/os.js';
-const meta = ref<Misskey.entities.DetailedInstanceMetadata>();
+const meta = ref<Misskey.entities.MetaResponse>();
os.api('meta', { detail: true }).then(gotMeta => {
meta.value = gotMeta;
diff --git a/packages/frontend/src/components/MkFileCaptionEditWindow.vue b/packages/frontend/src/components/MkFileCaptionEditWindow.vue
index b582b88712..b799fb9447 100644
--- a/packages/frontend/src/components/MkFileCaptionEditWindow.vue
+++ b/packages/frontend/src/components/MkFileCaptionEditWindow.vue
@@ -25,7 +25,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { } from 'vue';
+import { shallowRef, ref } from 'vue';
import * as Misskey from 'misskey-js';
import MkModalWindow from '@/components/MkModalWindow.vue';
import MkTextarea from '@/components/MkTextarea.vue';
@@ -42,12 +42,12 @@ const emit = defineEmits<{
(ev: 'closed'): void;
}>();
-const dialog = $shallowRef<InstanceType<typeof MkModalWindow>>();
+const dialog = shallowRef<InstanceType<typeof MkModalWindow>>();
-let caption = $ref(props.default);
+const caption = ref(props.default);
async function ok() {
- emit('done', caption);
- dialog.close();
+ emit('done', caption.value);
+ dialog.value.close();
}
</script>
diff --git a/packages/frontend/src/components/MkFolder.vue b/packages/frontend/src/components/MkFolder.vue
index 30e93ef9e4..03621a4255 100644
--- a/packages/frontend/src/components/MkFolder.vue
+++ b/packages/frontend/src/components/MkFolder.vue
@@ -50,7 +50,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { nextTick, onMounted } from 'vue';
+import { nextTick, onMounted, shallowRef, ref } from 'vue';
import { defaultStore } from '@/store.js';
const props = withDefaults(defineProps<{
@@ -70,10 +70,10 @@ const getBgColor = (el: HTMLElement) => {
}
};
-let rootEl = $shallowRef<HTMLElement>();
-let bgSame = $ref(false);
-let opened = $ref(props.defaultOpen);
-let openedAtLeastOnce = $ref(props.defaultOpen);
+const rootEl = shallowRef<HTMLElement>();
+const bgSame = ref(false);
+const opened = ref(props.defaultOpen);
+const openedAtLeastOnce = ref(props.defaultOpen);
function enter(el) {
const elementHeight = el.getBoundingClientRect().height;
@@ -98,20 +98,20 @@ function afterLeave(el) {
}
function toggle() {
- if (!opened) {
- openedAtLeastOnce = true;
+ if (!opened.value) {
+ openedAtLeastOnce.value = true;
}
nextTick(() => {
- opened = !opened;
+ opened.value = !opened.value;
});
}
onMounted(() => {
const computedStyle = getComputedStyle(document.documentElement);
- const parentBg = getBgColor(rootEl.parentElement);
+ const parentBg = getBgColor(rootEl.value.parentElement);
const myBg = computedStyle.getPropertyValue('--panel');
- bgSame = parentBg === myBg;
+ bgSame.value = parentBg === myBg;
});
</script>
diff --git a/packages/frontend/src/components/MkFollowButton.vue b/packages/frontend/src/components/MkFollowButton.vue
index eebb753db1..d1b1956a03 100644
--- a/packages/frontend/src/components/MkFollowButton.vue
+++ b/packages/frontend/src/components/MkFollowButton.vue
@@ -35,7 +35,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { onBeforeUnmount, onMounted } from 'vue';
+import { onBeforeUnmount, onMounted, ref } from 'vue';
import * as Misskey from 'misskey-js';
import * as os from '@/os.js';
import { useStream } from '@/stream.js';
@@ -57,9 +57,9 @@ const emit = defineEmits<{
(_: 'update:user', value: Misskey.entities.UserDetailed): void
}>();
-let isFollowing = $ref(props.user.isFollowing);
-let hasPendingFollowRequestFromYou = $ref(props.user.hasPendingFollowRequestFromYou);
-let wait = $ref(false);
+const isFollowing = ref(props.user.isFollowing);
+const hasPendingFollowRequestFromYou = ref(props.user.hasPendingFollowRequestFromYou);
+const wait = ref(false);
const connection = useStream().useChannel('main');
if (props.user.isFollowing == null) {
@@ -71,16 +71,16 @@ if (props.user.isFollowing == null) {
function onFollowChange(user: Misskey.entities.UserDetailed) {
if (user.id === props.user.id) {
- isFollowing = user.isFollowing;
- hasPendingFollowRequestFromYou = user.hasPendingFollowRequestFromYou;
+ isFollowing.value = user.isFollowing;
+ hasPendingFollowRequestFromYou.value = user.hasPendingFollowRequestFromYou;
}
}
async function onClick() {
- wait = true;
+ wait.value = true;
try {
- if (isFollowing) {
+ if (isFollowing.value) {
const { canceled } = await os.confirm({
type: 'warning',
text: i18n.t('unfollowConfirm', { name: props.user.name || props.user.username }),
@@ -92,11 +92,11 @@ async function onClick() {
userId: props.user.id,
});
} else {
- if (hasPendingFollowRequestFromYou) {
+ if (hasPendingFollowRequestFromYou.value) {
await os.api('following/requests/cancel', {
userId: props.user.id,
});
- hasPendingFollowRequestFromYou = false;
+ hasPendingFollowRequestFromYou.value = false;
} else {
await os.api('following/create', {
userId: props.user.id,
@@ -104,9 +104,9 @@ async function onClick() {
});
emit('update:user', {
...props.user,
- withReplies: defaultStore.state.defaultWithReplies
+ withReplies: defaultStore.state.defaultWithReplies,
});
- hasPendingFollowRequestFromYou = true;
+ hasPendingFollowRequestFromYou.value = true;
claimAchievement('following1');
@@ -127,7 +127,7 @@ async function onClick() {
} catch (err) {
console.error(err);
} finally {
- wait = false;
+ wait.value = false;
}
}
diff --git a/packages/frontend/src/components/MkForgotPassword.vue b/packages/frontend/src/components/MkForgotPassword.vue
index 521ac11d12..9b57688a02 100644
--- a/packages/frontend/src/components/MkForgotPassword.vue
+++ b/packages/frontend/src/components/MkForgotPassword.vue
@@ -39,7 +39,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { } from 'vue';
+import { ref } from 'vue';
import MkModalWindow from '@/components/MkModalWindow.vue';
import MkButton from '@/components/MkButton.vue';
import MkInput from '@/components/MkInput.vue';
@@ -53,19 +53,19 @@ const emit = defineEmits<{
(ev: 'closed'): void;
}>();
-let dialog: InstanceType<typeof MkModalWindow> = $ref();
+const dialog = ref<InstanceType<typeof MkModalWindow>>();
-let username = $ref('');
-let email = $ref('');
-let processing = $ref(false);
+const username = ref('');
+const email = ref('');
+const processing = ref(false);
async function onSubmit() {
- processing = true;
+ processing.value = true;
await os.apiWithDialog('request-reset-password', {
- username,
- email,
+ username: username.value,
+ email: email.value,
});
emit('done');
- dialog.close();
+ dialog.value.close();
}
</script>
diff --git a/packages/frontend/src/components/MkFormDialog.vue b/packages/frontend/src/components/MkFormDialog.vue
index 24404728ca..6f882cfab7 100644
--- a/packages/frontend/src/components/MkFormDialog.vue
+++ b/packages/frontend/src/components/MkFormDialog.vue
@@ -26,11 +26,11 @@ SPDX-License-Identifier: AGPL-3.0-only
<template #label><span v-text="form[item].label || item"></span><span v-if="form[item].required === false"> ({{ i18n.ts.optional }})</span></template>
<template v-if="form[item].description" #caption>{{ form[item].description }}</template>
</MkInput>
- <MkInput v-else-if="form[item].type === 'string' && !form[item].multiline" v-model="values[item]" type="text">
+ <MkInput v-else-if="form[item].type === 'string' && !form[item].multiline" v-model="values[item]" type="text" :mfmAutocomplete="form[item].treatAsMfm">
<template #label><span v-text="form[item].label || item"></span><span v-if="form[item].required === false"> ({{ i18n.ts.optional }})</span></template>
<template v-if="form[item].description" #caption>{{ form[item].description }}</template>
</MkInput>
- <MkTextarea v-else-if="form[item].type === 'string' && form[item].multiline" v-model="values[item]">
+ <MkTextarea v-else-if="form[item].type === 'string' && form[item].multiline" v-model="values[item]" :mfmAutocomplete="form[item].treatAsMfm" :mfmPreview="form[item].treatAsMfm">
<template #label><span v-text="form[item].label || item"></span><span v-if="form[item].required === false"> ({{ i18n.ts.optional }})</span></template>
<template v-if="form[item].description" #caption>{{ form[item].description }}</template>
</MkTextarea>
diff --git a/packages/frontend/src/components/MkGoogle.vue b/packages/frontend/src/components/MkGoogle.vue
index 185a49b5a9..c0b20507fc 100644
--- a/packages/frontend/src/components/MkGoogle.vue
+++ b/packages/frontend/src/components/MkGoogle.vue
@@ -23,7 +23,7 @@ const query = ref(props.q);
const search = () => {
const sp = new URLSearchParams();
sp.append('q', query.value);
- window.open(`https://www.google.com/search?${sp.toString()}`, '_blank');
+ window.open(`https://www.google.com/search?${sp.toString()}`, '_blank', 'noopener');
};
</script>
diff --git a/packages/frontend/src/components/MkHeatmap.vue b/packages/frontend/src/components/MkHeatmap.vue
index 0022531e58..a57e6c9292 100644
--- a/packages/frontend/src/components/MkHeatmap.vue
+++ b/packages/frontend/src/components/MkHeatmap.vue
@@ -13,7 +13,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { onMounted, nextTick, watch } from 'vue';
+import { onMounted, nextTick, watch, shallowRef, ref } from 'vue';
import { Chart } from 'chart.js';
import * as os from '@/os.js';
import { defaultStore } from '@/store.js';
@@ -27,11 +27,11 @@ const props = defineProps<{
src: string;
}>();
-const rootEl = $shallowRef<HTMLDivElement>(null);
-const chartEl = $shallowRef<HTMLCanvasElement>(null);
+const rootEl = shallowRef<HTMLDivElement>(null);
+const chartEl = shallowRef<HTMLCanvasElement>(null);
const now = new Date();
let chartInstance: Chart = null;
-let fetching = $ref(true);
+const fetching = ref(true);
const { handler: externalTooltipHandler } = useChartTooltip({
position: 'middle',
@@ -42,8 +42,8 @@ async function renderChart() {
chartInstance.destroy();
}
- const wide = rootEl.offsetWidth > 700;
- const narrow = rootEl.offsetWidth < 400;
+ const wide = rootEl.value.offsetWidth > 700;
+ const narrow = rootEl.value.offsetWidth < 400;
const weeks = wide ? 50 : narrow ? 10 : 25;
const chartLimit = 7 * weeks;
@@ -88,7 +88,7 @@ async function renderChart() {
values = raw.deliverFailed;
}
- fetching = false;
+ fetching.value = false;
await nextTick();
@@ -101,7 +101,7 @@ async function renderChart() {
const marginEachCell = 4;
- chartInstance = new Chart(chartEl, {
+ chartInstance = new Chart(chartEl.value, {
type: 'matrix',
data: {
datasets: [{
@@ -210,7 +210,7 @@ async function renderChart() {
}
watch(() => props.src, () => {
- fetching = true;
+ fetching.value = true;
renderChart();
});
diff --git a/packages/frontend/src/components/MkImgWithBlurhash.vue b/packages/frontend/src/components/MkImgWithBlurhash.vue
index 4fb573fdbc..942861e1f4 100644
--- a/packages/frontend/src/components/MkImgWithBlurhash.vue
+++ b/packages/frontend/src/components/MkImgWithBlurhash.vue
@@ -21,7 +21,6 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts">
-import { $ref } from 'vue/macros';
import DrawBlurhash from '@/workers/draw-blurhash?worker';
import TestWebGL2 from '@/workers/test-webgl2?worker';
import { WorkerMultiDispatch } from '@/scripts/worker-multi-dispatch.js';
@@ -58,7 +57,7 @@ const canvasPromise = new Promise<WorkerMultiDispatch | HTMLCanvasElement>(resol
</script>
<script lang="ts" setup>
-import { computed, nextTick, onMounted, onUnmounted, shallowRef, watch } from 'vue';
+import { computed, nextTick, onMounted, onUnmounted, shallowRef, watch, ref } from 'vue';
import { v4 as uuid } from 'uuid';
import { render } from 'buraha';
import { defaultStore } from '@/store.js';
@@ -98,41 +97,41 @@ const viewId = uuid();
const canvas = shallowRef<HTMLCanvasElement>();
const root = shallowRef<HTMLDivElement>();
const img = shallowRef<HTMLImageElement>();
-let loaded = $ref(false);
-let canvasWidth = $ref(64);
-let canvasHeight = $ref(64);
-let imgWidth = $ref(props.width);
-let imgHeight = $ref(props.height);
-let bitmapTmp = $ref<CanvasImageSource | undefined>();
-const hide = computed(() => !loaded || props.forceBlurhash);
+const loaded = ref(false);
+const canvasWidth = ref(64);
+const canvasHeight = ref(64);
+const imgWidth = ref(props.width);
+const imgHeight = ref(props.height);
+const bitmapTmp = ref<CanvasImageSource | undefined>();
+const hide = computed(() => !loaded.value || props.forceBlurhash);
function waitForDecode() {
if (props.src != null && props.src !== '') {
nextTick()
.then(() => img.value?.decode())
.then(() => {
- loaded = true;
+ loaded.value = true;
}, error => {
console.log('Error occurred during decoding image', img.value, error);
});
} else {
- loaded = false;
+ loaded.value = false;
}
}
watch([() => props.width, () => props.height, root], () => {
const ratio = props.width / props.height;
if (ratio > 1) {
- canvasWidth = Math.round(64 * ratio);
- canvasHeight = 64;
+ canvasWidth.value = Math.round(64 * ratio);
+ canvasHeight.value = 64;
} else {
- canvasWidth = 64;
- canvasHeight = Math.round(64 / ratio);
+ canvasWidth.value = 64;
+ canvasHeight.value = Math.round(64 / ratio);
}
const clientWidth = root.value?.clientWidth ?? 300;
- imgWidth = clientWidth;
- imgHeight = Math.round(clientWidth / ratio);
+ imgWidth.value = clientWidth;
+ imgHeight.value = Math.round(clientWidth / ratio);
}, {
immediate: true,
});
@@ -140,15 +139,15 @@ watch([() => props.width, () => props.height, root], () => {
function drawImage(bitmap: CanvasImageSource) {
// canvasがない(mountedされていない)場合はTmpに保存しておく
if (!canvas.value) {
- bitmapTmp = bitmap;
+ bitmapTmp.value = bitmap;
return;
}
// canvasがあれば描画する
- bitmapTmp = undefined;
+ bitmapTmp.value = undefined;
const ctx = canvas.value.getContext('2d');
if (!ctx) return;
- ctx.drawImage(bitmap, 0, 0, canvasWidth, canvasHeight);
+ ctx.drawImage(bitmap, 0, 0, canvasWidth.value, canvasHeight.value);
}
function drawAvg() {
@@ -160,7 +159,7 @@ function drawAvg() {
// avgColorでお茶をにごす
ctx.beginPath();
ctx.fillStyle = extractAvgColorFromBlurhash(props.hash) ?? '#888';
- ctx.fillRect(0, 0, canvasWidth, canvasHeight);
+ ctx.fillRect(0, 0, canvasWidth.value, canvasHeight.value);
}
async function draw() {
@@ -212,8 +211,8 @@ watch(() => props.hash, () => {
onMounted(() => {
// drawImageがmountedより先に呼ばれている場合はここで描画する
- if (bitmapTmp) {
- drawImage(bitmapTmp);
+ if (bitmapTmp.value) {
+ drawImage(bitmapTmp.value);
}
waitForDecode();
});
diff --git a/packages/frontend/src/components/MkInput.vue b/packages/frontend/src/components/MkInput.vue
index 6f237761a8..b4b4e1b0b7 100644
--- a/packages/frontend/src/components/MkInput.vue
+++ b/packages/frontend/src/components/MkInput.vue
@@ -43,11 +43,12 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { onMounted, nextTick, ref, shallowRef, watch, computed, toRefs } from 'vue';
+import { onMounted, onUnmounted, nextTick, ref, shallowRef, watch, computed, toRefs } from 'vue';
import { debounce } from 'throttle-debounce';
import MkButton from '@/components/MkButton.vue';
import { useInterval } from '@/scripts/use-interval.js';
import { i18n } from '@/i18n.js';
+import { Autocomplete, SuggestionType } from '@/scripts/autocomplete.js';
const props = defineProps<{
modelValue: string | number | null;
@@ -59,6 +60,7 @@ const props = defineProps<{
placeholder?: string;
autofocus?: boolean;
autocomplete?: string;
+ mfmAutocomplete?: boolean | SuggestionType[],
autocapitalize?: string;
spellcheck?: boolean;
step?: any;
@@ -93,6 +95,7 @@ const height =
props.small ? 33 :
props.large ? 39 :
36;
+let autocomplete: Autocomplete;
const focus = () => inputEl.value.focus();
const onInput = (ev: KeyboardEvent) => {
@@ -160,6 +163,16 @@ onMounted(() => {
focus();
}
});
+
+ if (props.mfmAutocomplete) {
+ autocomplete = new Autocomplete(inputEl.value, v, props.mfmAutocomplete === true ? null : props.mfmAutocomplete);
+ }
+});
+
+onUnmounted(() => {
+ if (autocomplete) {
+ autocomplete.detach();
+ }
});
defineExpose({
diff --git a/packages/frontend/src/components/MkInstanceCardMini.vue b/packages/frontend/src/components/MkInstanceCardMini.vue
index 6af9c6ccb5..9cde197e19 100644
--- a/packages/frontend/src/components/MkInstanceCardMini.vue
+++ b/packages/frontend/src/components/MkInstanceCardMini.vue
@@ -15,21 +15,22 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
+import { ref } from 'vue';
import * as Misskey from 'misskey-js';
import MkMiniChart from '@/components/MkMiniChart.vue';
import * as os from '@/os.js';
import { getProxiedImageUrlNullable } from '@/scripts/media-proxy.js';
const props = defineProps<{
- instance: Misskey.entities.Instance;
+ instance: Misskey.entities.FederationInstance;
}>();
-let chartValues = $ref<number[] | null>(null);
+const chartValues = ref<number[] | null>(null);
os.apiGet('charts/instance', { host: props.instance.host, limit: 16 + 1, span: 'day' }).then(res => {
// 今日のぶんの値はまだ途中の値であり、それも含めると大抵の場合前日よりも下降しているようなグラフになってしまうため今日は弾く
- res.requests.received.splice(0, 1);
- chartValues = res.requests.received;
+ res['requests.received'].splice(0, 1);
+ chartValues.value = res['requests.received'];
});
function getInstanceIcon(instance): string {
diff --git a/packages/frontend/src/components/MkInstanceStats.vue b/packages/frontend/src/components/MkInstanceStats.vue
index 509254de74..7b763ad385 100644
--- a/packages/frontend/src/components/MkInstanceStats.vue
+++ b/packages/frontend/src/components/MkInstanceStats.vue
@@ -84,7 +84,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { onMounted } from 'vue';
+import { onMounted, ref, shallowRef } from 'vue';
import { Chart } from 'chart.js';
import MkSelect from '@/components/MkSelect.vue';
import MkChart from '@/components/MkChart.vue';
@@ -100,11 +100,11 @@ import { initChart } from '@/scripts/init-chart.js';
initChart();
const chartLimit = 500;
-let chartSpan = $ref<'hour' | 'day'>('hour');
-let chartSrc = $ref('active-users');
-let heatmapSrc = $ref('active-users');
-let subDoughnutEl = $shallowRef<HTMLCanvasElement>();
-let pubDoughnutEl = $shallowRef<HTMLCanvasElement>();
+const chartSpan = ref<'hour' | 'day'>('hour');
+const chartSrc = ref('active-users');
+const heatmapSrc = ref('active-users');
+const subDoughnutEl = shallowRef<HTMLCanvasElement>();
+const pubDoughnutEl = shallowRef<HTMLCanvasElement>();
const { handler: externalTooltipHandler1 } = useChartTooltip({
position: 'middle',
@@ -163,7 +163,7 @@ function createDoughnut(chartEl, tooltip, data) {
onMounted(() => {
os.apiGet('federation/stats', { limit: 30 }).then(fedStats => {
- createDoughnut(subDoughnutEl, externalTooltipHandler1, fedStats.topSubInstances.map(x => ({
+ createDoughnut(subDoughnutEl.value, externalTooltipHandler1, fedStats.topSubInstances.map(x => ({
name: x.host,
color: x.themeColor,
value: x.followersCount,
@@ -172,7 +172,7 @@ onMounted(() => {
},
})).concat([{ name: '(other)', color: '#80808080', value: fedStats.otherFollowersCount }]));
- createDoughnut(pubDoughnutEl, externalTooltipHandler2, fedStats.topPubInstances.map(x => ({
+ createDoughnut(pubDoughnutEl.value, externalTooltipHandler2, fedStats.topPubInstances.map(x => ({
name: x.host,
color: x.themeColor,
value: x.followingCount,
diff --git a/packages/frontend/src/components/MkInstanceTicker.vue b/packages/frontend/src/components/MkInstanceTicker.vue
index f0650e48f1..e358a1c549 100644
--- a/packages/frontend/src/components/MkInstanceTicker.vue
+++ b/packages/frontend/src/components/MkInstanceTicker.vue
@@ -11,7 +11,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { } from 'vue';
+import { computed } from 'vue';
import { instanceName } from '@/config.js';
import { instance as Instance } from '@/instance.js';
import { getProxiedImageUrlNullable } from '@/scripts/media-proxy.js';
@@ -30,7 +30,7 @@ const instance = props.instance ?? {
themeColor: (document.querySelector('meta[name="theme-color-orig"]') as HTMLMetaElement).content,
};
-const faviconUrl = $computed(() => props.instance ? getProxiedImageUrlNullable(props.instance.faviconUrl, 'preview') : getProxiedImageUrlNullable(Instance.iconUrl, 'preview') ?? getProxiedImageUrlNullable(Instance.faviconUrl, 'preview') ?? '/favicon.ico');
+const faviconUrl = computed(() => props.instance ? getProxiedImageUrlNullable(props.instance.faviconUrl, 'preview') : getProxiedImageUrlNullable(Instance.iconUrl, 'preview') ?? getProxiedImageUrlNullable(Instance.faviconUrl, 'preview') ?? '/favicon.ico');
const themeColor = instance.themeColor ?? '#777777';
diff --git a/packages/frontend/src/components/MkInviteCode.vue b/packages/frontend/src/components/MkInviteCode.vue
index 8e3561e2b8..54d997d1c9 100644
--- a/packages/frontend/src/components/MkInviteCode.vue
+++ b/packages/frontend/src/components/MkInviteCode.vue
@@ -67,7 +67,7 @@ import { i18n } from '@/i18n.js';
import * as os from '@/os.js';
const props = defineProps<{
- invite: Misskey.entities.Invite;
+ invite: Misskey.entities.InviteCode;
moderator?: boolean;
}>();
diff --git a/packages/frontend/src/components/MkLaunchPad.vue b/packages/frontend/src/components/MkLaunchPad.vue
index 17f8af4f63..099082f539 100644
--- a/packages/frontend/src/components/MkLaunchPad.vue
+++ b/packages/frontend/src/components/MkLaunchPad.vue
@@ -27,7 +27,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { } from 'vue';
+import { shallowRef } from 'vue';
import MkModal from '@/components/MkModal.vue';
import { navbarItemDef } from '@/navbar.js';
import { defaultStore } from '@/store.js';
@@ -48,7 +48,7 @@ const preferedModalType = (deviceKind === 'desktop' && props.src != null) ? 'pop
deviceKind === 'smartphone' ? 'drawer' :
'dialog';
-const modal = $shallowRef<InstanceType<typeof MkModal>>();
+const modal = shallowRef<InstanceType<typeof MkModal>>();
const menu = defaultStore.state.menu;
@@ -63,7 +63,7 @@ const items = Object.keys(navbarItemDef).filter(k => !menu.includes(k)).map(k =>
}));
function close() {
- modal.close();
+ modal.value.close();
}
</script>
@@ -101,6 +101,8 @@ function close() {
vertical-align: bottom;
height: 100px;
border-radius: var(--radius);
+ padding: 10px;
+ box-sizing: border-box;
&:hover {
color: var(--accent);
diff --git a/packages/frontend/src/components/MkLink.vue b/packages/frontend/src/components/MkLink.vue
index 114b9b4faf..e16307c762 100644
--- a/packages/frontend/src/components/MkLink.vue
+++ b/packages/frontend/src/components/MkLink.vue
@@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template>
<component
- :is="self ? 'MkA' : 'a'" ref="el" style="word-break: break-all;" class="_link" :[attr]="self ? url.substring(local.length) : url" :rel="rel" :target="target"
+ :is="self ? 'MkA' : 'a'" ref="el" style="word-break: break-all;" class="_link" :[attr]="self ? url.substring(local.length) : url" :rel="rel ?? 'nofollow noopener'" :target="target"
:title="url"
>
<slot></slot>
@@ -14,7 +14,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { defineAsyncComponent } from 'vue';
+import { defineAsyncComponent, ref } from 'vue';
import { url as local } from '@/config.js';
import { useTooltip } from '@/scripts/use-tooltip.js';
import * as os from '@/os.js';
@@ -29,13 +29,13 @@ const self = props.url.startsWith(local);
const attr = self ? 'to' : 'href';
const target = self ? null : '_blank';
-const el = $ref();
+const el = ref();
-useTooltip($$(el), (showing) => {
+useTooltip(el, (showing) => {
os.popup(defineAsyncComponent(() => import('@/components/MkUrlPreviewPopup.vue')), {
showing,
url: props.url,
- source: el,
+ source: el.value,
}, {}, 'closed');
});
</script>
diff --git a/packages/frontend/src/components/MkMediaBanner.vue b/packages/frontend/src/components/MkMediaBanner.vue
index 42a709ae26..4594c8a1db 100644
--- a/packages/frontend/src/components/MkMediaBanner.vue
+++ b/packages/frontend/src/components/MkMediaBanner.vue
@@ -32,7 +32,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { onMounted, shallowRef, watch } from 'vue';
+import { shallowRef, watch, ref } from 'vue';
import * as Misskey from 'misskey-js';
import { i18n } from '@/i18n.js';
@@ -42,7 +42,7 @@ const props = withDefaults(defineProps<{
});
const audioEl = shallowRef<HTMLAudioElement>();
-let hide = $ref(true);
+const hide = ref(true);
watch(audioEl, () => {
if (audioEl.value) {
diff --git a/packages/frontend/src/components/MkMediaImage.vue b/packages/frontend/src/components/MkMediaImage.vue
index 1fa42c1e48..0040f00dc2 100644
--- a/packages/frontend/src/components/MkMediaImage.vue
+++ b/packages/frontend/src/components/MkMediaImage.vue
@@ -19,7 +19,7 @@ SPDX-License-Identifier: AGPL-3.0-only
>
<ImgWithBlurhash
:hash="image.blurhash"
- :src="(defaultStore.state.enableDataSaverMode && hide) ? null : url"
+ :src="(defaultStore.state.dataSaver.media && hide) ? null : url"
:forceBlurhash="hide"
:cover="hide || cover"
:alt="image.comment || image.name"
@@ -32,8 +32,8 @@ SPDX-License-Identifier: AGPL-3.0-only
<template v-if="hide">
<div :class="$style.hiddenText">
<div :class="$style.hiddenTextWrapper">
- <b v-if="image.isSensitive" style="display: block;"><i class="ph-eye-closed ph-bold ph-lg"></i> {{ i18n.ts.sensitive }}{{ defaultStore.state.enableDataSaverMode ? ` (${i18n.ts.image}${image.size ? ' ' + bytes(image.size) : ''})` : '' }}</b>
- <b v-else style="display: block;"><i class="ph-image-square ph-bold ph-lg"></i> {{ defaultStore.state.enableDataSaverMode && image.size ? bytes(image.size) : i18n.ts.image }}</b>
+ <b v-if="image.isSensitive" style="display: block;"><i class="ph-eye-closed ph-bold ph-lg"></i> {{ i18n.ts.sensitive }}{{ defaultStore.state.dataSaver.media ? ` (${i18n.ts.image}${image.size ? ' ' + bytes(image.size) : ''})` : '' }}</b>
+ <b v-else style="display: block;"><i class="ph-image-square ph-bold ph-lg"></i> {{ defaultStore.state.dataSaver.media && image.size ? bytes(image.size) : i18n.ts.image }}</b>
<span v-if="controls" style="display: block;">{{ i18n.ts.clickToShow }}</span>
</div>
</div>
@@ -52,7 +52,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { watch } from 'vue';
+import { watch, ref, computed } from 'vue';
import * as Misskey from 'misskey-js';
import { getStaticImageUrl } from '@/scripts/media-proxy.js';
import bytes from '@/filters/bytes.js';
@@ -74,10 +74,10 @@ const props = withDefaults(defineProps<{
controls: true,
});
-let hide = $ref(true);
-let darkMode: boolean = $ref(defaultStore.state.darkMode);
+const hide = ref(true);
+const darkMode = ref<boolean>(defaultStore.state.darkMode);
-const url = $computed(() => (props.raw || defaultStore.state.loadRawImages)
+const url = computed(() => (props.raw || defaultStore.state.loadRawImages)
? props.image.url
: defaultStore.state.disableShowingAnimatedImages
? getStaticImageUrl(props.image.url)
@@ -88,14 +88,14 @@ function onclick() {
if (!props.controls) {
return;
}
- if (hide) {
- hide = false;
+ if (hide.value) {
+ hide.value = false;
}
}
// Plugin:register_note_view_interruptor を使って書き換えられる可能性があるためwatchする
watch(() => props.image, () => {
- hide = (defaultStore.state.nsfw === 'force' || defaultStore.state.enableDataSaverMode) ? true : (props.image.isSensitive && defaultStore.state.nsfw !== 'ignore');
+ hide.value = (defaultStore.state.nsfw === 'force' || defaultStore.state.dataSaver.media) ? true : (props.image.isSensitive && defaultStore.state.nsfw !== 'ignore');
}, {
deep: true,
immediate: true,
@@ -106,7 +106,7 @@ function showMenu(ev: MouseEvent) {
text: i18n.ts.hide,
icon: 'ph-eye-slash ph-bold ph-lg',
action: () => {
- hide = true;
+ hide.value = true;
},
}, ...(iAmModerator ? [{
text: i18n.ts.markAsSensitive,
diff --git a/packages/frontend/src/components/MkMediaList.vue b/packages/frontend/src/components/MkMediaList.vue
index 610978e4ab..46e32ef2d8 100644
--- a/packages/frontend/src/components/MkMediaList.vue
+++ b/packages/frontend/src/components/MkMediaList.vue
@@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
-<div ref="root" :class="$style.root">
+<div :class="$style.root">
<XBanner v-for="media in mediaList.filter(media => !previewable(media))" :key="media.id" :media="media"/>
<div v-if="mediaList.filter(media => previewable(media)).length > 0" :class="$style.container">
<div
@@ -28,43 +28,8 @@ SPDX-License-Identifier: AGPL-3.0-only
</div>
</template>
-<script lang="ts">
-/**
- * アスペクト比算出のためにHTMLElement.clientWidthを使うが、
- * 大変重たいのでコンテナ要素とメディアリスト幅のペアをキャッシュする
- * (タイムラインごとにスクロールコンテナが存在する前提だが……)
- */
-const widthCache = new Map<Element, number>();
-
-/**
- * コンテナ要素がリサイズされたらキャッシュを削除する
- */
-const ro = new ResizeObserver(entries => {
- for (const entry of entries) {
- widthCache.delete(entry.target);
- }
-});
-
-async function getClientWidthWithCache(targetEl: HTMLElement, containerEl: HTMLElement, count = 0) {
- if (_DEV_) console.log('getClientWidthWithCache', { targetEl, containerEl, count, cache: widthCache.get(containerEl) });
- if (widthCache.has(containerEl)) return widthCache.get(containerEl)!;
-
- const width = targetEl.clientWidth;
-
- if (count <= 10 && width < 64) {
- // widthが64未満はおかしいのでリトライする
- await new Promise(resolve => setTimeout(resolve, 50));
- return getClientWidthWithCache(targetEl, containerEl, count + 1);
- }
-
- widthCache.set(containerEl, width);
- ro.observe(containerEl);
- return width;
-}
-</script>
-
<script lang="ts" setup>
-import { onMounted, onUnmounted, shallowRef } from 'vue';
+import { computed, onMounted, onUnmounted, shallowRef } from 'vue';
import * as Misskey from 'misskey-js';
import PhotoSwipeLightbox from 'photoswipe/lightbox';
import PhotoSwipe from 'photoswipe';
@@ -76,19 +41,16 @@ import XModPlayer from '@/components/MkModPlayer.vue';
import * as os from '@/os.js';
import { FILE_TYPE_BROWSERSAFE, FILE_EXT_TRACKER_MODULES, FILE_TYPE_TRACKER_MODULES } from '@/const.js';
import { defaultStore } from '@/store.js';
-import { getScrollContainer, getBodyScrollHeight } from '@/scripts/scroll.js';
const props = defineProps<{
mediaList: Misskey.entities.DriveFile[];
raw?: boolean;
}>();
-const root = shallowRef<HTMLDivElement>();
-const container = shallowRef<HTMLElement | null | undefined>(undefined);
const gallery = shallowRef<HTMLDivElement>();
const pswpZIndex = os.claimZIndex('middle');
document.documentElement.style.setProperty('--mk-pswp-root-z-index', pswpZIndex.toString());
-const count = $computed(() => props.mediaList.filter(media => previewable(media)).length);
+const count = computed(() => props.mediaList.filter(media => previewable(media)).length);
let lightbox: PhotoSwipeLightbox | null;
const popstateHandler = (): void => {
@@ -97,12 +59,8 @@ const popstateHandler = (): void => {
}
};
-/**
- * アスペクト比をmediaListWithOneImageAppearanceに基づいていい感じに調整する
- * aspect-ratioではなくheightを使う
- */
async function calcAspectRatio() {
- if (!gallery.value || !root.value) return;
+ if (!gallery.value) return;
let img = props.mediaList[0];
@@ -111,41 +69,22 @@ async function calcAspectRatio() {
return;
}
- if (!container.value) container.value = getScrollContainer(root.value);
- const width = container.value ? await getClientWidthWithCache(root.value, container.value) : root.value.clientWidth;
-
- const heightMin = (ratio: number) => {
- const imgResizeRatio = width / img.properties.width;
- const imgDrawHeight = img.properties.height * imgResizeRatio;
- const maxHeight = width * ratio;
- const height = Math.min(imgDrawHeight, maxHeight);
- if (_DEV_) console.log('Image height calculated:', { width, properties: img.properties, imgResizeRatio, imgDrawHeight, maxHeight, height });
- return `${height}px`;
- };
+ const ratioMax = (ratio: number) => `${Math.max(ratio, img.properties.width / img.properties.height).toString()} / 1`;
switch (defaultStore.state.mediaListWithOneImageAppearance) {
case '16_9':
- gallery.value.style.height = heightMin(9 / 16);
+ gallery.value.style.aspectRatio = ratioMax(16 / 9);
break;
case '1_1':
- gallery.value.style.height = heightMin(1);
+ gallery.value.style.aspectRatio = ratioMax(1 / 1);
break;
case '2_3':
- gallery.value.style.height = heightMin(3 / 2);
+ gallery.value.style.aspectRatio = ratioMax(2 / 3);
break;
- default: {
- const maxHeight = Math.max(64, (container.value ? container.value.clientHeight : getBodyScrollHeight()) * 0.5 || 360);
- if (width === 0 || !maxHeight) return;
- const imgResizeRatio = width / img.properties.width;
- const imgDrawHeight = img.properties.height * imgResizeRatio;
- gallery.value.style.height = `${Math.max(64, Math.min(imgDrawHeight, maxHeight))}px`;
- gallery.value.style.minHeight = 'initial';
- gallery.value.style.maxHeight = 'initial';
+ default:
+ gallery.value.style.aspectRatio = '';
break;
- }
}
-
- gallery.value.style.aspectRatio = 'initial';
}
const isModule = (file: Misskey.entities.DriveFile): boolean => {
diff --git a/packages/frontend/src/components/MkMediaVideo.vue b/packages/frontend/src/components/MkMediaVideo.vue
index 33a9b0fbf9..4f8560f0f1 100644
--- a/packages/frontend/src/components/MkMediaVideo.vue
+++ b/packages/frontend/src/components/MkMediaVideo.vue
@@ -7,8 +7,8 @@ SPDX-License-Identifier: AGPL-3.0-only
<div v-if="hide" :class="[$style.hidden, (video.isSensitive && defaultStore.state.highlightSensitiveMedia) && $style.sensitiveContainer]" @click="hide = false">
<!-- 【注意】dataSaverMode が有効になっている際には、hide が false になるまでサムネイルや動画を読み込まないようにすること -->
<div :class="$style.sensitive">
- <b v-if="video.isSensitive" style="display: block;"><i class="ph-warning ph-bold ph-lg"></i> {{ i18n.ts.sensitive }}{{ defaultStore.state.enableDataSaverMode ? ` (${i18n.ts.video}${video.size ? ' ' + bytes(video.size) : ''})` : '' }}</b>
- <b v-else style="display: block;"><i class="ph-film-strip ph-bold ph-lg"></i> {{ defaultStore.state.enableDataSaverMode && video.size ? bytes(video.size) : i18n.ts.video }}</b>
+ <b v-if="video.isSensitive" style="display: block;"><i class="ph-warning ph-bold ph-lg"></i> {{ i18n.ts.sensitive }}{{ defaultStore.state.dataSaver.media ? ` (${i18n.ts.video}${video.size ? ' ' + bytes(video.size) : ''})` : '' }}</b>
+ <b v-else style="display: block;"><i class="ph-film-strip ph-bold ph-lg"></i> {{ defaultStore.state.dataSaver.media && video.size ? bytes(video.size) : i18n.ts.video }}</b>
<span>{{ i18n.ts.clickToShow }}</span>
</div>
</div>
@@ -37,18 +37,25 @@ import * as Misskey from 'misskey-js';
import bytes from '@/filters/bytes.js';
import { defaultStore } from '@/store.js';
import { i18n } from '@/i18n.js';
+import hasAudio from '@/scripts/media-has-audio.js';
const props = defineProps<{
video: Misskey.entities.DriveFile;
}>();
-const hide = ref((defaultStore.state.nsfw === 'force' || defaultStore.state.enableDataSaverMode) ? true : (props.video.isSensitive && defaultStore.state.nsfw !== 'ignore'));
+const hide = ref((defaultStore.state.nsfw === 'force' || defaultStore.state.dataSaver.media) ? true : (props.video.isSensitive && defaultStore.state.nsfw !== 'ignore'));
const videoEl = shallowRef<HTMLVideoElement>();
watch(videoEl, () => {
if (videoEl.value) {
videoEl.value.volume = 0.3;
+ hasAudio(videoEl.value).then(had => {
+ if (!had) {
+ videoEl.value.loop = videoEl.value.muted = true;
+ videoEl.value.play();
+ }
+ });
}
});
</script>
diff --git a/packages/frontend/src/components/MkMenu.vue b/packages/frontend/src/components/MkMenu.vue
index 83f56dc1a2..b0f997a1b9 100644
--- a/packages/frontend/src/components/MkMenu.vue
+++ b/packages/frontend/src/components/MkMenu.vue
@@ -13,9 +13,9 @@ SPDX-License-Identifier: AGPL-3.0-only
@contextmenu.self="e => e.preventDefault()"
>
<template v-for="(item, i) in items2">
- <div v-if="item === null" role="separator" :class="$style.divider"></div>
+ <div v-if="item.type === 'divider'" role="separator" :class="$style.divider"></div>
<span v-else-if="item.type === 'label'" role="menuitem" :class="[$style.label, $style.item]">
- <span>{{ item.text }}</span>
+ <span style="opacity: 0.7;">{{ item.text }}</span>
</span>
<span v-else-if="item.type === 'pending'" role="menuitem" :tabindex="i" :class="[$style.pending, $style.item]">
<span><MkEllipsis/></span>
@@ -23,32 +23,44 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkA v-else-if="item.type === 'link'" role="menuitem" :to="item.to" :tabindex="i" class="_button" :class="$style.item" @click.passive="close(true)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)">
<i v-if="item.icon" class="ti-fw" :class="[$style.icon, item.icon]"></i>
<MkAvatar v-if="item.avatar" :user="item.avatar" :class="$style.avatar"/>
- <span>{{ item.text }}</span>
- <span v-if="item.indicate" :class="$style.indicator"><i class="_indicatorCircle"></i></span>
+ <div :class="$style.item_content">
+ <span :class="$style.item_content_text">{{ item.text }}</span>
+ <span v-if="item.indicate" :class="$style.indicator"><i class="_indicatorCircle"></i></span>
+ </div>
</MkA>
<a v-else-if="item.type === 'a'" role="menuitem" :href="item.href" :target="item.target" :download="item.download" :tabindex="i" class="_button" :class="$style.item" @click="close(true)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)">
<i v-if="item.icon" class="ti-fw" :class="[$style.icon, item.icon]"></i>
- <span>{{ item.text }}</span>
- <span v-if="item.indicate" :class="$style.indicator"><i class="_indicatorCircle"></i></span>
+ <div :class="$style.item_content">
+ <span :class="$style.item_content_text">{{ item.text }}</span>
+ <span v-if="item.indicate" :class="$style.indicator"><i class="_indicatorCircle"></i></span>
+ </div>
</a>
<button v-else-if="item.type === 'user'" role="menuitem" :tabindex="i" class="_button" :class="[$style.item, { [$style.active]: item.active }]" :disabled="item.active" @click="clicked(item.action, $event)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)">
<MkAvatar :user="item.user" :class="$style.avatar"/><MkUserName :user="item.user"/>
- <span v-if="item.indicate" :class="$style.indicator"><i class="_indicatorCircle"></i></span>
+ <div v-if="item.indicate" :class="$style.item_content">
+ <span :class="$style.indicator"><i class="_indicatorCircle"></i></span>
+ </div>
</button>
<button v-else-if="item.type === 'switch'" role="menuitemcheckbox" :tabindex="i" class="_button" :class="[$style.item, $style.switch, { [$style.switchDisabled]: item.disabled } ]" @click="switchItem(item)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)">
<MkSwitchButton :class="$style.switchButton" :checked="item.ref" :disabled="item.disabled" @toggle="switchItem(item)"/>
- <span :class="$style.switchText">{{ item.text }}</span>
+ <div :class="$style.item_content">
+ <span :class="[$style.item_content_text, $style.switchText]">{{ item.text }}</span>
+ </div>
</button>
<button v-else-if="item.type === 'parent'" class="_button" role="menuitem" :tabindex="i" :class="[$style.item, $style.parent, { [$style.childShowing]: childShowingItem === item }]" @mouseenter="preferClick ? null : showChildren(item, $event)" @click="!preferClick ? null : showChildren(item, $event)">
<i v-if="item.icon" class="ti-fw" :class="[$style.icon, item.icon]" style="pointer-events: none;"></i>
- <span style="pointer-events: none;">{{ item.text }}</span>
- <span :class="$style.caret" style="pointer-events: none;"><i class="ph-caret-right ph-bold ph-lg ti-fw"></i></span>
+ <div :class="$style.item_content">
+ <span :class="$style.item_content_text" style="pointer-events: none;">{{ item.text }}</span>
+ <span :class="$style.caret" style="pointer-events: none;"><i class="ph-caret-right ph-bold ph-lg ti-fw"></i></span>
+ </div>
</button>
<button v-else :tabindex="i" class="_button" role="menuitem" :class="[$style.item, { [$style.danger]: item.danger, [$style.active]: item.active }]" :disabled="item.active" @click="clicked(item.action, $event)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)">
<i v-if="item.icon" class="ti-fw" :class="[$style.icon, item.icon]"></i>
<MkAvatar v-if="item.avatar" :user="item.avatar" :class="$style.avatar"/>
- <span>{{ item.text }}</span>
- <span v-if="item.indicate" :class="$style.indicator"><i class="_indicatorCircle"></i></span>
+ <div :class="$style.item_content">
+ <span :class="$style.item_content_text">{{ item.text }}</span>
+ <span v-if="item.indicate" :class="$style.indicator"><i class="_indicatorCircle"></i></span>
+ </div>
</button>
</template>
<span v-if="items2.length === 0" :class="[$style.none, $style.item]">
@@ -62,7 +74,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts">
-import { Ref, defineAsyncComponent, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue';
+import { computed, defineAsyncComponent, nextTick, onBeforeUnmount, onMounted, ref, shallowRef, watch } from 'vue';
import { focusPrev, focusNext } from '@/scripts/focus.js';
import MkSwitchButton from '@/components/MkSwitch.button.vue';
import { MenuItem, InnerMenuItem, MenuPending, MenuAction, MenuSwitch, MenuParent } from '@/types/menu';
@@ -90,19 +102,19 @@ const emit = defineEmits<{
(ev: 'hide'): void;
}>();
-let itemsEl = $shallowRef<HTMLDivElement>();
+const itemsEl = shallowRef<HTMLDivElement>();
-let items2: InnerMenuItem[] = $ref([]);
+const items2 = ref<InnerMenuItem[]>([]);
-let child = $shallowRef<InstanceType<typeof XChild>>();
+const child = shallowRef<InstanceType<typeof XChild>>();
-let keymap = $computed(() => ({
+const keymap = computed(() => ({
'up|k|shift+tab': focusUp,
'down|j|tab': focusDown,
'esc': close,
}));
-let childShowingItem = $ref<MenuItem | null>();
+const childShowingItem = ref<MenuItem | null>();
let preferClick = isTouchUsing || props.asDrawer;
@@ -115,22 +127,22 @@ watch(() => props.items, () => {
if (item && 'then' in item) { // if item is Promise
items[i] = { type: 'pending' };
item.then(actualItem => {
- items2[i] = actualItem;
+ items2.value[i] = actualItem;
});
}
}
- items2 = items as InnerMenuItem[];
+ items2.value = items as InnerMenuItem[];
}, {
immediate: true,
});
const childMenu = ref<MenuItem[] | null>();
-let childTarget = $shallowRef<HTMLElement | null>();
+const childTarget = shallowRef<HTMLElement | null>();
function closeChild() {
childMenu.value = null;
- childShowingItem = null;
+ childShowingItem.value = null;
}
function childActioned() {
@@ -139,8 +151,8 @@ function childActioned() {
}
const onGlobalMousedown = (event: MouseEvent) => {
- if (childTarget && (event.target === childTarget || childTarget.contains(event.target))) return;
- if (child && child.checkHit(event)) return;
+ if (childTarget.value && (event.target === childTarget.value || childTarget.value.contains(event.target))) return;
+ if (child.value && child.value.checkHit(event)) return;
closeChild();
};
@@ -177,10 +189,10 @@ async function showChildren(item: MenuParent, ev: MouseEvent) {
});
emit('hide');
} else {
- childTarget = ev.currentTarget ?? ev.target;
+ childTarget.value = ev.currentTarget ?? ev.target;
// これでもリアクティビティは保たれる
childMenu.value = children;
- childShowingItem = item;
+ childShowingItem.value = item;
}
}
@@ -202,14 +214,14 @@ function focusDown() {
}
function switchItem(item: MenuSwitch & { ref: any }) {
- if (item.disabled) return;
+ if (item.disabled !== undefined && (typeof item.disabled === 'boolean' ? item.disabled : item.disabled.value)) return;
item.ref = !item.ref;
}
onMounted(() => {
if (props.viaKeyboard) {
nextTick(() => {
- if (itemsEl) focusNext(itemsEl.children[0], true, false);
+ if (itemsEl.value) focusNext(itemsEl.value.children[0], true, false);
});
}
@@ -228,6 +240,7 @@ onBeforeUnmount(() => {
.root {
padding: 8px 0;
box-sizing: border-box;
+ max-width: 100vw;
min-width: 200px;
overflow: auto;
overscroll-behavior: contain;
@@ -267,7 +280,8 @@ onBeforeUnmount(() => {
}
.item {
- display: block;
+ display: flex;
+ align-items: center;
position: relative;
padding: 5px 16px;
width: 100%;
@@ -340,10 +354,6 @@ onBeforeUnmount(() => {
pointer-events: none;
font-size: 0.7em;
padding-bottom: 4px;
-
- > span {
- opacity: 0.7;
- }
}
&.pending {
@@ -373,6 +383,22 @@ onBeforeUnmount(() => {
}
}
+.item_content {
+ width: 100%;
+ max-width: 100vw;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 8px;
+ text-overflow: ellipsis;
+}
+
+.item_content_text {
+ max-width: calc(100vw - 4rem);
+ text-overflow: ellipsis;
+ overflow: hidden;
+}
+
.switch {
position: relative;
display: flex;
@@ -406,6 +432,7 @@ onBeforeUnmount(() => {
.icon {
margin-right: 8px;
+ line-height: 1;
}
.caret {
@@ -419,9 +446,8 @@ onBeforeUnmount(() => {
}
.indicator {
- position: absolute;
- top: 5px;
- left: 13px;
+ display: flex;
+ align-items: center;
color: var(--indicator);
font-size: 12px;
animation: blink 1s infinite;
diff --git a/packages/frontend/src/components/MkMiniChart.vue b/packages/frontend/src/components/MkMiniChart.vue
index 8d2a147306..f0a2c232bd 100644
--- a/packages/frontend/src/components/MkMiniChart.vue
+++ b/packages/frontend/src/components/MkMiniChart.vue
@@ -31,7 +31,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { watch } from 'vue';
+import { watch, ref } from 'vue';
import { v4 as uuid } from 'uuid';
import tinycolor from 'tinycolor2';
import { useInterval } from '@/scripts/use-interval.js';
@@ -43,11 +43,11 @@ const props = defineProps<{
const viewBoxX = 50;
const viewBoxY = 50;
const gradientId = uuid();
-let polylinePoints = $ref('');
-let polygonPoints = $ref('');
-let headX = $ref<number | null>(null);
-let headY = $ref<number | null>(null);
-let clock = $ref<number | null>(null);
+const polylinePoints = ref('');
+const polygonPoints = ref('');
+const headX = ref<number | null>(null);
+const headY = ref<number | null>(null);
+const clock = ref<number | null>(null);
const accent = tinycolor(getComputedStyle(document.documentElement).getPropertyValue('--accent'));
const color = accent.toRgbString();
@@ -60,12 +60,12 @@ function draw(): void {
(1 - (n / peak)) * viewBoxY,
]);
- polylinePoints = _polylinePoints.map(xy => `${xy[0]},${xy[1]}`).join(' ');
+ polylinePoints.value = _polylinePoints.map(xy => `${xy[0]},${xy[1]}`).join(' ');
- polygonPoints = `0,${ viewBoxY } ${ polylinePoints } ${ viewBoxX },${ viewBoxY }`;
+ polygonPoints.value = `0,${ viewBoxY } ${ polylinePoints.value } ${ viewBoxX },${ viewBoxY }`;
- headX = _polylinePoints.at(-1)![0];
- headY = _polylinePoints.at(-1)![1];
+ headX.value = _polylinePoints.at(-1)![0];
+ headY.value = _polylinePoints.at(-1)![1];
}
watch(() => props.src, draw, { immediate: true });
diff --git a/packages/frontend/src/components/MkModPlayer.vue b/packages/frontend/src/components/MkModPlayer.vue
index c24eaab2fa..055522d466 100644
--- a/packages/frontend/src/components/MkModPlayer.vue
+++ b/packages/frontend/src/components/MkModPlayer.vue
@@ -29,7 +29,7 @@
</template>
<script lang="ts" setup>
-import { ref, nextTick } from 'vue';
+import { ref, nextTick, computed } from 'vue';
import * as Misskey from 'misskey-js';
import { i18n } from '@/i18n.js';
import { defaultStore } from '@/store.js';
@@ -71,9 +71,9 @@ const props = defineProps<{
module: Misskey.entities.DriveFile
}>();
-const isSensitive = $computed(() => { return props.module.isSensitive; });
-const url = $computed(() => { return props.module.url; });
-let hide = ref((defaultStore.state.nsfw === 'force') ? true : isSensitive && (defaultStore.state.nsfw !== 'ignore'));
+const isSensitive = computed(() => { return props.module.isSensitive; });
+const url = computed(() => { return props.module.url; });
+let hide = ref((defaultStore.state.nsfw === 'force') ? true : isSensitive.value && (defaultStore.state.nsfw !== 'ignore'));
let playing = ref(false);
let displayCanvas = ref<HTMLCanvasElement>();
let progress = ref<HTMLProgressElement>();
@@ -84,7 +84,7 @@ const rowBuffer = 24;
let buffer = null;
let isSeeking = false;
-player.value.load(url).then((result) => {
+player.value.load(url.value).then((result) => {
buffer = result;
try {
player.value.play(buffer);
diff --git a/packages/frontend/src/components/MkModal.vue b/packages/frontend/src/components/MkModal.vue
index ec5039c504..5cd31cdf7c 100644
--- a/packages/frontend/src/components/MkModal.vue
+++ b/packages/frontend/src/components/MkModal.vue
@@ -42,7 +42,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { nextTick, normalizeClass, onMounted, onUnmounted, provide, watch } from 'vue';
+import { nextTick, normalizeClass, onMounted, onUnmounted, provide, watch, ref, shallowRef, computed } from 'vue';
import * as os from '@/os.js';
import { isTouchUsing } from '@/scripts/touch.js';
import { defaultStore } from '@/store.js';
@@ -89,14 +89,14 @@ const emit = defineEmits<{
provide('modal', true);
-let maxHeight = $ref<number>();
-let fixed = $ref(false);
-let transformOrigin = $ref('center');
-let showing = $ref(true);
-let content = $shallowRef<HTMLElement>();
+const maxHeight = ref<number>();
+const fixed = ref(false);
+const transformOrigin = ref('center');
+const showing = ref(true);
+const content = shallowRef<HTMLElement>();
const zIndex = os.claimZIndex(props.zPriority);
-let useSendAnime = $ref(false);
-const type = $computed<ModalTypes>(() => {
+const useSendAnime = ref(false);
+const type = computed<ModalTypes>(() => {
if (props.preferType === 'auto') {
if (!defaultStore.state.disableDrawer && isTouchUsing && deviceKind === 'smartphone') {
return 'drawer';
@@ -107,26 +107,26 @@ const type = $computed<ModalTypes>(() => {
return props.preferType!;
}
});
-const isEnableBgTransparent = $computed(() => props.transparentBg && (type === 'popup'));
-let transitionName = $computed((() =>
+const isEnableBgTransparent = computed(() => props.transparentBg && (type.value === 'popup'));
+const transitionName = computed((() =>
defaultStore.state.animation
- ? useSendAnime
+ ? useSendAnime.value
? 'send'
- : type === 'drawer'
+ : type.value === 'drawer'
? 'modal-drawer'
- : type === 'popup'
+ : type.value === 'popup'
? 'modal-popup'
: 'modal'
: ''
));
-let transitionDuration = $computed((() =>
- transitionName === 'send'
+const transitionDuration = computed((() =>
+ transitionName.value === 'send'
? 400
- : transitionName === 'modal-popup'
+ : transitionName.value === 'modal-popup'
? 100
- : transitionName === 'modal'
+ : transitionName.value === 'modal'
? 200
- : transitionName === 'modal-drawer'
+ : transitionName.value === 'modal-drawer'
? 200
: 0
));
@@ -135,12 +135,12 @@ let contentClicking = false;
function close(opts: { useSendAnimation?: boolean } = {}) {
if (opts.useSendAnimation) {
- useSendAnime = true;
+ useSendAnime.value = true;
}
// eslint-disable-next-line vue/no-mutating-props
if (props.src) props.src.style.pointerEvents = 'auto';
- showing = false;
+ showing.value = false;
emit('close');
}
@@ -149,8 +149,8 @@ function onBgClick() {
emit('click');
}
-if (type === 'drawer') {
- maxHeight = window.innerHeight / 1.5;
+if (type.value === 'drawer') {
+ maxHeight.value = window.innerHeight / 1.5;
}
const keymap = {
@@ -162,21 +162,21 @@ const SCROLLBAR_THICKNESS = 16;
const align = () => {
if (props.src == null) return;
- if (type === 'drawer') return;
- if (type === 'dialog') return;
+ if (type.value === 'drawer') return;
+ if (type.value === 'dialog') return;
- if (content == null) return;
+ if (content.value == null) return;
const srcRect = props.src.getBoundingClientRect();
- const width = content!.offsetWidth;
- const height = content!.offsetHeight;
+ const width = content.value!.offsetWidth;
+ const height = content.value!.offsetHeight;
let left;
let top;
- const x = srcRect.left + (fixed ? 0 : window.pageXOffset);
- const y = srcRect.top + (fixed ? 0 : window.pageYOffset);
+ const x = srcRect.left + (fixed.value ? 0 : window.pageXOffset);
+ const y = srcRect.top + (fixed.value ? 0 : window.pageYOffset);
if (props.anchor.x === 'center') {
left = x + (props.src.offsetWidth / 2) - (width / 2);
@@ -194,7 +194,7 @@ const align = () => {
top = y + props.src.offsetHeight;
}
- if (fixed) {
+ if (fixed.value) {
// 画面から横にはみ出る場合
if (left + width > (window.innerWidth - SCROLLBAR_THICKNESS)) {
left = (window.innerWidth - SCROLLBAR_THICKNESS) - width;
@@ -207,16 +207,16 @@ const align = () => {
if (top + height > ((window.innerHeight - SCROLLBAR_THICKNESS) - MARGIN)) {
if (props.noOverlap && props.anchor.x === 'center') {
if (underSpace >= (upperSpace / 3)) {
- maxHeight = underSpace;
+ maxHeight.value = underSpace;
} else {
- maxHeight = upperSpace;
+ maxHeight.value = upperSpace;
top = (upperSpace + MARGIN) - height;
}
} else {
top = ((window.innerHeight - SCROLLBAR_THICKNESS) - MARGIN) - height;
}
} else {
- maxHeight = underSpace;
+ maxHeight.value = underSpace;
}
} else {
// 画面から横にはみ出る場合
@@ -231,16 +231,16 @@ const align = () => {
if (top + height - window.pageYOffset > ((window.innerHeight - SCROLLBAR_THICKNESS) - MARGIN)) {
if (props.noOverlap && props.anchor.x === 'center') {
if (underSpace >= (upperSpace / 3)) {
- maxHeight = underSpace;
+ maxHeight.value = underSpace;
} else {
- maxHeight = upperSpace;
+ maxHeight.value = upperSpace;
top = window.pageYOffset + ((upperSpace + MARGIN) - height);
}
} else {
top = ((window.innerHeight - SCROLLBAR_THICKNESS) - MARGIN) - height + window.pageYOffset - 1;
}
} else {
- maxHeight = underSpace;
+ maxHeight.value = underSpace;
}
}
@@ -255,29 +255,29 @@ const align = () => {
let transformOriginX = 'center';
let transformOriginY = 'center';
- if (top >= srcRect.top + props.src.offsetHeight + (fixed ? 0 : window.pageYOffset)) {
+ if (top >= srcRect.top + props.src.offsetHeight + (fixed.value ? 0 : window.pageYOffset)) {
transformOriginY = 'top';
- } else if ((top + height) <= srcRect.top + (fixed ? 0 : window.pageYOffset)) {
+ } else if ((top + height) <= srcRect.top + (fixed.value ? 0 : window.pageYOffset)) {
transformOriginY = 'bottom';
}
- if (left >= srcRect.left + props.src.offsetWidth + (fixed ? 0 : window.pageXOffset)) {
+ if (left >= srcRect.left + props.src.offsetWidth + (fixed.value ? 0 : window.pageXOffset)) {
transformOriginX = 'left';
- } else if ((left + width) <= srcRect.left + (fixed ? 0 : window.pageXOffset)) {
+ } else if ((left + width) <= srcRect.left + (fixed.value ? 0 : window.pageXOffset)) {
transformOriginX = 'right';
}
- transformOrigin = `${transformOriginX} ${transformOriginY}`;
+ transformOrigin.value = `${transformOriginX} ${transformOriginY}`;
- content.style.left = left + 'px';
- content.style.top = top + 'px';
+ content.value.style.left = left + 'px';
+ content.value.style.top = top + 'px';
};
const onOpened = () => {
emit('opened');
// モーダルコンテンツにマウスボタンが押され、コンテンツ外でマウスボタンが離されたときにモーダルバックグラウンドクリックと判定させないためにマウスイベントを監視しフラグ管理する
- const el = content!.children[0];
+ const el = content.value!.children[0];
el.addEventListener('mousedown', ev => {
contentClicking = true;
window.addEventListener('mouseup', ev => {
@@ -299,7 +299,7 @@ onMounted(() => {
// eslint-disable-next-line vue/no-mutating-props
props.src.style.pointerEvents = 'none';
}
- fixed = (type === 'drawer') || (getFixedContainer(props.src) != null);
+ fixed.value = (type.value === 'drawer') || (getFixedContainer(props.src) != null);
await nextTick();
@@ -307,7 +307,7 @@ onMounted(() => {
}, { immediate: true });
nextTick(() => {
- alignObserver.observe(content!);
+ alignObserver.observe(content.value!);
});
});
diff --git a/packages/frontend/src/components/MkModalWindow.vue b/packages/frontend/src/components/MkModalWindow.vue
index 800950ea82..b91988304d 100644
--- a/packages/frontend/src/components/MkModalWindow.vue
+++ b/packages/frontend/src/components/MkModalWindow.vue
@@ -22,7 +22,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { onMounted, onUnmounted } from 'vue';
+import { onMounted, onUnmounted, shallowRef, ref } from 'vue';
import MkModal from './MkModal.vue';
const props = withDefaults(defineProps<{
@@ -44,14 +44,14 @@ const emit = defineEmits<{
(event: 'ok'): void;
}>();
-let modal = $shallowRef<InstanceType<typeof MkModal>>();
-let rootEl = $shallowRef<HTMLElement>();
-let headerEl = $shallowRef<HTMLElement>();
-let bodyWidth = $ref(0);
-let bodyHeight = $ref(0);
+const modal = shallowRef<InstanceType<typeof MkModal>>();
+const rootEl = shallowRef<HTMLElement>();
+const headerEl = shallowRef<HTMLElement>();
+const bodyWidth = ref(0);
+const bodyHeight = ref(0);
const close = () => {
- modal.close();
+ modal.value.close();
};
const onBgClick = () => {
@@ -67,14 +67,14 @@ const onKeydown = (evt) => {
};
const ro = new ResizeObserver((entries, observer) => {
- bodyWidth = rootEl.offsetWidth;
- bodyHeight = rootEl.offsetHeight - headerEl.offsetHeight;
+ bodyWidth.value = rootEl.value.offsetWidth;
+ bodyHeight.value = rootEl.value.offsetHeight - headerEl.value.offsetHeight;
});
onMounted(() => {
- bodyWidth = rootEl.offsetWidth;
- bodyHeight = rootEl.offsetHeight - headerEl.offsetHeight;
- ro.observe(rootEl);
+ bodyWidth.value = rootEl.value.offsetWidth;
+ bodyHeight.value = rootEl.value.offsetHeight - headerEl.value.offsetHeight;
+ ro.observe(rootEl.value);
});
onUnmounted(() => {
diff --git a/packages/frontend/src/components/MkNote.vue b/packages/frontend/src/components/MkNote.vue
index 74edc8903e..9ecf21071d 100644
--- a/packages/frontend/src/components/MkNote.vue
+++ b/packages/frontend/src/components/MkNote.vue
@@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template>
<div
- v-if="!muted"
+ v-if="!hardMuted && !muted"
v-show="!isDeleted"
ref="el"
v-hotkey="keymap"
@@ -50,14 +50,14 @@ SPDX-License-Identifier: AGPL-3.0-only
<div v-if="appearNote.channel" :class="$style.colorBar" :style="{ background: appearNote.channel.color }"></div>
<MkAvatar :class="$style.avatar" :user="appearNote.user" :link="!mock" :preview="!mock"/>
<div :class="[$style.main, { [$style.clickToOpen]: defaultStore.state.clickToOpen }]" @click="defaultStore.state.clickToOpen ? noteclick(appearNote.id) : undefined">
- <MkNoteHeader :note="appearNote" :mini="true" v-on:click.stop/>
+ <MkNoteHeader :note="appearNote" :mini="true" @click.stop/>
<MkInstanceTicker v-if="showTicker" :instance="appearNote.user.instance"/>
<div style="container-type: inline-size;">
<p v-if="appearNote.cw != null" :class="$style.cw">
<Mfm v-if="appearNote.cw != ''" style="margin-right: 8px;" :text="appearNote.cw" :author="appearNote.user" :nyaize="'respect'"/>
- <MkCwButton v-model="showContent" :note="appearNote" style="margin: 4px 0;" v-on:click.stop/>
+ <MkCwButton v-model="showContent" :text="appearNote.text" :renote="appearNote.renote" :files="appearNote.files" :poll="appearNote.poll" style="margin: 4px 0;" @click.stop/>
</p>
- <div v-show="appearNote.cw == null || showContent" :class="[{ [$style.contentCollapsed]: collapsed }]" >
+ <div v-show="appearNote.cw == null || showContent" :class="[{ [$style.contentCollapsed]: collapsed }]">
<div :class="$style.text">
<span v-if="appearNote.isHidden" style="opacity: 0.5">({{ i18n.ts.private }})</span>
<MkA v-if="appearNote.replyId" :class="$style.replyIcon" :to="`/notes/${appearNote.replyId}`"><i class="ph-arrow-bend-left-up ph-bold ph-lg"></i></MkA>
@@ -79,31 +79,31 @@ SPDX-License-Identifier: AGPL-3.0-only
<Mfm :text="translation.text" :author="appearNote.user" :nyaize="'respect'" :emojiUrls="appearNote.emojis"/>
</div>
</div>
- <MkButton v-if="!allowAnim && animated" :class="$style.playMFMButton" :small="true" @click="animatedMFM()" v-on:click.stop><i class="ph-play ph-bold ph-lg "></i> {{ i18n.ts._animatedMFM.play }}</MkButton>
- <MkButton v-else-if="!defaultStore.state.animatedMfm && allowAnim && animated" :class="$style.playMFMButton" :small="true" @click="animatedMFM()" v-on:click.stop><i class="ph-stop ph-bold ph-lg "></i> {{ i18n.ts._animatedMFM.stop }}</MkButton>
+ <MkButton v-if="!allowAnim && animated" :class="$style.playMFMButton" :small="true" @click="animatedMFM()" @click.stop><i class="ph-play ph-bold ph-lg "></i> {{ i18n.ts._animatedMFM.play }}</MkButton>
+ <MkButton v-else-if="!defaultStore.state.animatedMfm && allowAnim && animated" :class="$style.playMFMButton" :small="true" @click="animatedMFM()" @click.stop><i class="ph-stop ph-bold ph-lg "></i> {{ i18n.ts._animatedMFM.stop }}</MkButton>
</div>
<div v-if="appearNote.files.length > 0">
- <MkMediaList :mediaList="appearNote.files" v-on:click.stop/>
+ <MkMediaList :mediaList="appearNote.files" @click.stop/>
</div>
- <MkPoll v-if="appearNote.poll" :note="appearNote" :class="$style.poll" v-on:click.stop />
- <MkUrlPreview v-for="url in urls" :key="url" :url="url" :compact="true" :detail="false" :class="$style.urlPreview" v-on:click.stop/>
+ <MkPoll v-if="appearNote.poll" :note="appearNote" :class="$style.poll" @click.stop/>
+ <MkUrlPreview v-for="url in urls" :key="url" :url="url" :compact="true" :detail="false" :class="$style.urlPreview" @click.stop/>
<div v-if="appearNote.renote" :class="$style.quote"><MkNoteSimple :note="appearNote.renote" :class="$style.quoteNote"/></div>
- <button v-if="isLong && collapsed" :class="$style.collapsed" class="_button" v-on:click.stop @click="collapsed = false">
+ <button v-if="isLong && collapsed" :class="$style.collapsed" class="_button" @click.stop @click="collapsed = false">
<span :class="$style.collapsedLabel">{{ i18n.ts.showMore }}</span>
</button>
- <button v-else-if="isLong && !collapsed" :class="$style.showLess" class="_button" v-on:click.stop @click="collapsed = true">
+ <button v-else-if="isLong && !collapsed" :class="$style.showLess" class="_button" @click.stop @click="collapsed = true">
<span :class="$style.showLessLabel">{{ i18n.ts.showLess }}</span>
</button>
</div>
<MkA v-if="appearNote.channel && !inChannel" :class="$style.channel" :to="`/channels/${appearNote.channel.id}`"><i class="ph-television ph-bold ph-lg"></i> {{ appearNote.channel.name }}</MkA>
</div>
- <MkReactionsViewer :note="appearNote" :maxNumber="16" v-on:click.stop @mockUpdateMyReaction="emitUpdReaction">
+ <MkReactionsViewer :note="appearNote" :maxNumber="16" @click.stop @mockUpdateMyReaction="emitUpdReaction">
<template #more>
<div :class="$style.reactionOmitted">{{ i18n.ts.more }}</div>
</template>
</MkReactionsViewer>
<footer :class="$style.footer">
- <button :class="$style.footerButton" class="_button" v-on:click.stop @click="reply()">
+ <button :class="$style.footerButton" class="_button" @click.stop @click="reply()">
<i class="ph-arrow-u-up-left ph-bold ph-lg"></i>
<p v-if="appearNote.repliesCount > 0" :class="$style.footerButtonCount">{{ appearNote.repliesCount }}</p>
</button>
@@ -113,7 +113,7 @@ SPDX-License-Identifier: AGPL-3.0-only
:class="$style.footerButton"
class="_button"
:style="renoted ? 'color: var(--accent) !important;' : ''"
- v-on:click.stop
+ @click.stop
@mousedown="renoted ? undoRenote(appearNote) : boostVisibility()"
>
<i class="ph-rocket-launch ph-bold ph-lg"></i>
@@ -127,19 +127,19 @@ SPDX-License-Identifier: AGPL-3.0-only
ref="quoteButton"
:class="$style.footerButton"
class="_button"
- v-on:click.stop
+ @click.stop
@mousedown="quote()"
>
<i class="ph-quotes ph-bold ph-lg"></i>
</button>
- <button v-if="appearNote.myReaction == null && appearNote.reactionAcceptance !== 'likeOnly'" ref="likeButton" :class="$style.footerButton" class="_button" v-on:click.stop @click="like()">
+ <button v-if="appearNote.myReaction == null && appearNote.reactionAcceptance !== 'likeOnly'" ref="likeButton" :class="$style.footerButton" class="_button" @click.stop @click="like()">
<i class="ph-heart ph-bold ph-lg"></i>
</button>
<button v-if="appearNote.myReaction == null" ref="reactButton" :class="$style.footerButton" class="_button" @mousedown="react()">
<i v-if="appearNote.reactionAcceptance === 'likeOnly'" class="ph-heart ph-bold ph-lg"></i>
<i v-else class="ph-smiley ph-bold ph-lg"></i>
</button>
- <button v-if="appearNote.myReaction != null" ref="reactButton" :class="$style.footerButton" class="_button" v-on:click.stop @click="undoReact(appearNote)">
+ <button v-if="appearNote.myReaction != null" ref="reactButton" :class="$style.footerButton" class="_button" @click.stop @click="undoReact(appearNote)">
<i class="ph-minus ph-bold ph-lg"></i>
</button>
<button v-if="defaultStore.state.showClipButtonInNoteFooter" ref="clipButton" :class="$style.footerButton" class="_button" @mousedown="clip()">
@@ -152,7 +152,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</div>
</article>
</div>
-<div v-else :class="$style.muted" @click="muted = false">
+<div v-else-if="!hardMuted" :class="$style.muted" @click="muted = false">
<I18n :src="i18n.ts.userSaysSomething" tag="small">
<template #name>
<MkA v-user-preview="appearNote.userId" :to="userPage(appearNote.user)">
@@ -161,10 +161,16 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
</I18n>
</div>
+<div v-else>
+ <!--
+ MkDateSeparatedList uses TransitionGroup which requires single element in the child elements
+ so MkNote create empty div instead of no elements
+ -->
+</div>
</template>
<script lang="ts" setup>
-import { computed, inject, onMounted, ref, shallowRef, Ref, defineAsyncComponent, watch, provide } from 'vue';
+import { computed, inject, onMounted, ref, shallowRef, Ref, watch, provide } from 'vue';
import * as mfm from '@sharkey/sfm-js';
import * as Misskey from 'misskey-js';
import MkNoteSub from '@/components/MkNoteSub.vue';
@@ -183,6 +189,7 @@ import { focusPrev, focusNext } from '@/scripts/focus.js';
import { checkWordMute } from '@/scripts/check-word-mute.js';
import { userPage } from '@/filters/user.js';
import * as os from '@/os.js';
+import * as sound from '@/scripts/sound.js';
import { defaultStore, noteViewInterruptors } from '@/store.js';
import { reactionPicker } from '@/scripts/reaction-picker.js';
import { extractUrlFromMfm } from '@/scripts/extract-url-from-mfm.js';
@@ -206,6 +213,7 @@ const props = withDefaults(defineProps<{
note: Misskey.entities.Note;
pinned?: boolean;
mock?: boolean;
+ withHardMute?: boolean;
}>(), {
mock: false,
});
@@ -222,7 +230,7 @@ const router = useRouter();
const inChannel = inject('inChannel', null);
const currentClip = inject<Ref<Misskey.entities.Clip> | null>('currentClip', null);
-let note = $ref(deepClone(props.note));
+const note = ref(deepClone(props.note));
function noteclick(id: string) {
const selection = document.getSelection();
@@ -234,7 +242,7 @@ function noteclick(id: string) {
// plugin
if (noteViewInterruptors.length > 0) {
onMounted(async () => {
- let result: Misskey.entities.Note | null = deepClone(note);
+ let result: Misskey.entities.Note | null = deepClone(note.value);
for (const interruptor of noteViewInterruptors) {
try {
result = await interruptor.handler(result);
@@ -246,15 +254,16 @@ if (noteViewInterruptors.length > 0) {
console.error(err);
}
}
- note = result;
+ note.value = result;
});
}
const isRenote = (
- note.renote != null &&
- note.text == null &&
- note.fileIds.length === 0 &&
- note.poll == null
+ note.value.renote != null &&
+ note.value.text == null &&
+ note.value.cw == null &&
+ note.value.fileIds.length === 0 &&
+ note.value.poll == null
);
const el = shallowRef<HTMLElement>();
@@ -266,27 +275,37 @@ const reactButton = shallowRef<HTMLElement>();
const quoteButton = shallowRef<HTMLElement>();
const clipButton = shallowRef<HTMLElement>();
const likeButton = shallowRef<HTMLElement>();
-let appearNote = $computed(() => isRenote ? note.renote as Misskey.entities.Note : note);
-const renoteUrl = appearNote.renote ? appearNote.renote.url : null;
-const renoteUri = appearNote.renote ? appearNote.renote.uri : null;
+const appearNote = computed(() => isRenote ? note.value.renote as Misskey.entities.Note : note.value);
+const renoteUrl = appearNote.value.renote ? appearNote.value.renote.url : null;
+const renoteUri = appearNote.value.renote ? appearNote.value.renote.uri : null;
-const isMyRenote = $i && ($i.id === note.userId);
+const isMyRenote = $i && ($i.id === note.value.userId);
const showContent = ref(defaultStore.state.uncollapseCW);
-const parsed = $computed(() => appearNote.text ? mfm.parse(appearNote.text) : null);
-const urls = $computed(() => parsed ? extractUrlFromMfm(parsed).filter(u => u !== renoteUrl && u !== renoteUri) : null);
-const animated = $computed(() => parsed ? checkAnimationFromMfm(parsed) : null);
-const allowAnim = ref(defaultStore.state.advancedMfm && defaultStore.state.animatedMfm ? true : false);
-const isLong = shouldCollapsed(appearNote, urls ?? []);
-const collapsed = defaultStore.state.expandLongNote && appearNote.cw == null ? false : ref(appearNote.cw == null && isLong);
+const parsed = computed(() => appearNote.value.text ? mfm.parse(appearNote.value.text).filter(u => u !== renoteUrl && u !== renoteUri) : null);
+const urls = computed(() => parsed.value ? extractUrlFromMfm(parsed.value) : null);
+const isLong = shouldCollapsed(appearNote.value, urls.value ?? []);
+const collapsed = defaultStore.state.expandLongNote && appearNote.value.cw == null ? false : ref(appearNote.value.cw == null && isLong);
const isDeleted = ref(false);
const renoted = ref(false);
-const muted = ref($i ? checkWordMute(appearNote, $i, $i.mutedWords) : false);
+const muted = ref(checkMute(appearNote.value, $i?.mutedWords));
+const hardMuted = ref(props.withHardMute && checkMute(appearNote.value, $i?.hardMutedWords));
const translation = ref<any>(null);
const translating = ref(false);
-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));
-let renoteCollapsed = $ref(defaultStore.state.collapseRenotes && isRenote && (($i && ($i.id === note.userId || $i.id === appearNote.userId)) || (appearNote.myReaction != null)));
+const showTicker = (defaultStore.state.instanceTicker === 'always') || (defaultStore.state.instanceTicker === 'remote' && appearNote.value.user.instance);
+const canRenote = computed(() => ['public', 'home'].includes(appearNote.value.visibility) || (appearNote.value.visibility === 'followers' && appearNote.value.userId === $i.id));
+const renoteCollapsed = ref(defaultStore.state.collapseRenotes && isRenote && (($i && ($i.id === note.value.userId || $i.id === appearNote.value.userId)) || (appearNote.value.myReaction != null)));
const defaultLike = computed(() => defaultStore.state.like ? defaultStore.state.like : null);
+const animated = computed(() => parsed.value ? checkAnimationFromMfm(parsed.value) : null);
+const allowAnim = ref(defaultStore.state.advancedMfm && defaultStore.state.animatedMfm ? true : false);
+
+function checkMute(note: Misskey.entities.Note, mutedWords: Array<string | string[]> | undefined | null): boolean {
+ if (mutedWords == null) return false;
+
+ if (checkWordMute(note, $i, mutedWords)) return true;
+ if (note.reply && checkWordMute(note.reply, $i, mutedWords)) return true;
+ if (note.renote && checkWordMute(note.renote, $i, mutedWords)) return true;
+ return false;
+}
const keymap = {
'r': () => reply(true),
@@ -301,20 +320,20 @@ const keymap = {
provide('react', (reaction: string) => {
os.api('notes/reactions/create', {
- noteId: appearNote.id,
+ noteId: appearNote.value.id,
reaction: reaction,
});
});
if (props.mock) {
watch(() => props.note, (to) => {
- note = deepClone(to);
+ note.value = deepClone(to);
}, { deep: true });
} else {
useNoteCapture({
rootEl: el,
- note: $$(appearNote),
- pureNote: $$(note),
+ note: appearNote,
+ pureNote: note,
isDeletedRef: isDeleted,
});
}
@@ -322,7 +341,7 @@ if (props.mock) {
if (!props.mock) {
useTooltip(renoteButton, async (showing) => {
const renotes = await os.api('notes/renotes', {
- noteId: appearNote.id,
+ noteId: appearNote.value.id,
limit: 11,
});
@@ -333,14 +352,14 @@ if (!props.mock) {
os.popup(MkUsersTooltip, {
showing,
users,
- count: appearNote.renoteCount,
+ count: appearNote.value.renoteCount,
targetElement: renoteButton.value,
}, {}, 'closed');
});
useTooltip(quoteButton, async (showing) => {
const renotes = await os.api('notes/renotes', {
- noteId: appearNote.id,
+ noteId: appearNote.value.id,
limit: 11,
quote: true,
});
@@ -352,14 +371,14 @@ if (!props.mock) {
os.popup(MkUsersTooltip, {
showing,
users,
- count: appearNote.renoteCount,
+ count: appearNote.value.renoteCount,
targetElement: quoteButton.value,
}, {}, 'closed');
});
if ($i) {
- os.api("notes/renotes", {
- noteId: appearNote.id,
+ os.api('notes/renotes', {
+ noteId: appearNote.value.id,
userId: $i.id,
limit: 1,
}).then((res) => {
@@ -419,7 +438,7 @@ function renote(visibility: Visibility | 'local') {
pleaseLogin();
showMovedDialog();
- if (appearNote.channel) {
+ if (appearNote.value.channel) {
const el = renoteButton.value as HTMLElement | null | undefined;
if (el) {
const rect = el.getBoundingClientRect();
@@ -430,14 +449,14 @@ function renote(visibility: Visibility | 'local') {
if (!props.mock) {
os.api('notes/create', {
- renoteId: appearNote.id,
- channelId: appearNote.channelId,
+ renoteId: appearNote.value.id,
+ channelId: appearNote.value.channelId,
}).then(() => {
os.toast(i18n.ts.renoted);
renoted.value = true;
});
}
- } else if (!appearNote.channel || appearNote.channel?.allowRenoteToExternal) {
+ } else if (!appearNote.value.channel || appearNote.value.channel?.allowRenoteToExternal) {
const el = renoteButton.value as HTMLElement | null | undefined;
if (el) {
const rect = el.getBoundingClientRect();
@@ -449,16 +468,16 @@ function renote(visibility: Visibility | 'local') {
const configuredVisibility = defaultStore.state.rememberNoteVisibility ? defaultStore.state.visibility : defaultStore.state.defaultNoteVisibility;
const localOnlySetting = defaultStore.state.rememberNoteVisibility ? defaultStore.state.localOnly : defaultStore.state.defaultNoteLocalOnly;
- let noteVisibility = visibility === 'local' || visibility === 'specified' ? smallerVisibility(appearNote.visibility, configuredVisibility) : smallerVisibility(visibility, configuredVisibility);
- if (appearNote.channel?.isSensitive) {
- noteVisibility = smallerVisibility(visibility === 'local' || visibility === 'specified' ? appearNote.visibility : visibility, 'home');
+ let noteVisibility = visibility === 'local' || visibility === 'specified' ? smallerVisibility(appearNote.value.visibility, configuredVisibility) : smallerVisibility(visibility, configuredVisibility);
+ if (appearNote.value.channel?.isSensitive) {
+ noteVisibility = smallerVisibility(visibility === 'local' || visibility === 'specified' ? appearNote.value.visibility : visibility, 'home');
}
if (!props.mock) {
os.api('notes/create', {
localOnly: visibility === 'local' ? true : localOnlySetting,
visibility: noteVisibility,
- renoteId: appearNote.id,
+ renoteId: appearNote.value.id,
}).then(() => {
os.toast(i18n.ts.renoted);
renoted.value = true;
@@ -474,13 +493,13 @@ function quote() {
return;
}
- if (appearNote.channel) {
+ if (appearNote.value.channel) {
os.post({
- renote: appearNote,
- channel: appearNote.channel,
+ renote: appearNote.value,
+ channel: appearNote.value.channel,
}).then(() => {
- os.api("notes/renotes", {
- noteId: appearNote.id,
+ os.api('notes/renotes', {
+ noteId: appearNote.value.id,
userId: $i.id,
limit: 1,
quote: true,
@@ -499,10 +518,10 @@ function quote() {
});
} else {
os.post({
- renote: appearNote,
+ renote: appearNote.value,
}).then(() => {
- os.api("notes/renotes", {
- noteId: appearNote.id,
+ os.api('notes/renotes', {
+ noteId: appearNote.value.id,
userId: $i.id,
limit: 1,
quote: true,
@@ -528,8 +547,8 @@ function reply(viaKeyboard = false): void {
return;
}
os.post({
- reply: appearNote,
- channel: appearNote.channel,
+ reply: appearNote.value,
+ channel: appearNote.value.channel,
animation: !viaKeyboard,
}, () => {
focus();
@@ -543,7 +562,7 @@ function like(): void {
return;
}
os.api('notes/like', {
- noteId: appearNote.id,
+ noteId: appearNote.value.id,
override: defaultLike.value,
});
const el = likeButton.value as HTMLElement | null | undefined;
@@ -558,13 +577,15 @@ function like(): void {
function react(viaKeyboard = false): void {
pleaseLogin();
showMovedDialog();
- if (appearNote.reactionAcceptance === 'likeOnly') {
+ if (appearNote.value.reactionAcceptance === 'likeOnly') {
+ sound.play('reaction');
+
if (props.mock) {
return;
}
os.api('notes/like', {
- noteId: appearNote.id,
+ noteId: appearNote.value.id,
override: defaultLike.value,
});
const el = reactButton.value as HTMLElement | null | undefined;
@@ -577,16 +598,18 @@ function react(viaKeyboard = false): void {
} else {
blur();
reactionPicker.show(reactButton.value, reaction => {
+ sound.play('reaction');
+
if (props.mock) {
emit('reaction', reaction);
return;
}
os.api('notes/reactions/create', {
- noteId: appearNote.id,
+ noteId: appearNote.value.id,
reaction: reaction,
});
- if (appearNote.text && appearNote.text.length > 100 && (Date.now() - new Date(appearNote.createdAt).getTime() < 1000 * 3)) {
+ if (appearNote.value.text && appearNote.value.text.length > 100 && (Date.now() - new Date(appearNote.value.createdAt).getTime() < 1000 * 3)) {
claimAchievement('reactWithoutRead');
}
}, () => {
@@ -613,8 +636,8 @@ function undoRenote(note) : void {
if (props.mock) {
return;
}
- os.api("notes/unrenote", {
- noteId: note.id
+ os.api('notes/unrenote', {
+ noteId: note.id,
});
os.toast(i18n.ts.rmboost);
renoted.value = false;
@@ -648,7 +671,7 @@ function onContextmenu(ev: MouseEvent): void {
ev.preventDefault();
react();
} else {
- const { menu, cleanup } = getNoteMenu({ note: note, translating, translation, menuButton, isDeleted, currentClip: currentClip?.value });
+ const { menu, cleanup } = getNoteMenu({ note: note.value, translating, translation, menuButton, isDeleted, currentClip: currentClip?.value });
os.contextMenu(menu, ev).then(focus).finally(cleanup);
}
}
@@ -658,14 +681,14 @@ function menu(viaKeyboard = false): void {
return;
}
- const { menu, cleanup } = getNoteMenu({ note: note, translating, translation, menuButton, isDeleted, currentClip: currentClip?.value });
+ const { menu, cleanup } = getNoteMenu({ note: note.value, translating, translation, menuButton, isDeleted, currentClip: currentClip?.value });
os.popupMenu(menu, menuButton.value, {
viaKeyboard,
}).then(focus).finally(cleanup);
}
async function menuVersions(viaKeyboard = false): Promise<void> {
- const { menu, cleanup } = await getNoteVersionsMenu({ note: note, menuVersionsButton });
+ const { menu, cleanup } = await getNoteVersionsMenu({ note: note.value, menuVersionsButton });
os.popupMenu(menu, menuVersionsButton.value, {
viaKeyboard,
}).then(focus).finally(cleanup);
@@ -676,7 +699,7 @@ async function clip() {
return;
}
- os.popupMenu(await getNoteClipMenu({ note: note, isDeleted, currentClip: currentClip?.value }), clipButton.value).then(focus);
+ os.popupMenu(await getNoteClipMenu({ note: note.value, isDeleted, currentClip: currentClip?.value }), clipButton.value).then(focus);
}
function showRenoteMenu(viaKeyboard = false): void {
@@ -691,7 +714,7 @@ function showRenoteMenu(viaKeyboard = false): void {
danger: true,
action: () => {
os.api('notes/delete', {
- noteId: note.id,
+ noteId: note.value.id,
});
isDeleted.value = true;
},
@@ -701,17 +724,17 @@ function showRenoteMenu(viaKeyboard = false): void {
if (isMyRenote) {
pleaseLogin();
os.popupMenu([
- getCopyNoteLinkMenu(note, i18n.ts.copyLinkRenote),
- null,
+ getCopyNoteLinkMenu(note.value, i18n.ts.copyLinkRenote),
+ { type: 'divider' },
getUnrenote(),
], renoteTime.value, {
viaKeyboard: viaKeyboard,
});
} else {
os.popupMenu([
- getCopyNoteLinkMenu(note, i18n.ts.copyLinkRenote),
- null,
- getAbuseNoteMenu(note, i18n.ts.reportAbuseRenote),
+ getCopyNoteLinkMenu(note.value, i18n.ts.copyLinkRenote),
+ { type: 'divider' },
+ getAbuseNoteMenu(note.value, i18n.ts.reportAbuseRenote),
$i.isModerator || $i.isAdmin ? getUnrenote() : undefined,
], renoteTime.value, {
viaKeyboard: viaKeyboard,
@@ -749,7 +772,7 @@ function focusAfter() {
function readPromo() {
os.api('promo/read', {
- noteId: appearNote.id,
+ noteId: appearNote.value.id,
});
isDeleted.value = true;
}
diff --git a/packages/frontend/src/components/MkNoteDetailed.vue b/packages/frontend/src/components/MkNoteDetailed.vue
index 93e39ff033..f29b9db6ae 100644
--- a/packages/frontend/src/components/MkNoteDetailed.vue
+++ b/packages/frontend/src/components/MkNoteDetailed.vue
@@ -69,7 +69,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<div :class="$style.noteContent">
<p v-if="appearNote.cw != null" :class="$style.cw">
<Mfm v-if="appearNote.cw != ''" style="margin-right: 8px;" :text="appearNote.cw" :author="appearNote.user" :nyaize="'respect'"/>
- <MkCwButton v-model="showContent" :note="appearNote"/>
+ <MkCwButton v-model="showContent" :text="appearNote.text" :files="appearNote.files" :poll="appearNote.poll"/>
</p>
<div v-show="appearNote.cw == null || showContent">
<span v-if="appearNote.isHidden" style="opacity: 0.5">({{ i18n.ts.private }})</span>
@@ -93,8 +93,8 @@ SPDX-License-Identifier: AGPL-3.0-only
<Mfm :text="translation.text" :author="appearNote.user" :nyaize="'respect'" :emojiUrls="appearNote.emojis"/>
</div>
</div>
- <MkButton v-if="!allowAnim && animated" :class="$style.playMFMButton" :small="true" @click="animatedMFM()" v-on:click.stop><i class="ph-play ph-bold ph-lg "></i> {{ i18n.ts._animatedMFM.play }}</MkButton>
- <MkButton v-else-if="!defaultStore.state.animatedMfm && allowAnim && animated" :class="$style.playMFMButton" :small="true" @click="animatedMFM()" v-on:click.stop><i class="ph-stop ph-bold ph-lg "></i> {{ i18n.ts._animatedMFM.stop }}</MkButton>
+ <MkButton v-if="!allowAnim && animated" :class="$style.playMFMButton" :small="true" @click="animatedMFM()" @click.stop><i class="ph-play ph-bold ph-lg "></i> {{ i18n.ts._animatedMFM.play }}</MkButton>
+ <MkButton v-else-if="!defaultStore.state.animatedMfm && allowAnim && animated" :class="$style.playMFMButton" :small="true" @click="animatedMFM()" @click.stop><i class="ph-stop ph-bold ph-lg "></i> {{ i18n.ts._animatedMFM.stop }}</MkButton>
<div v-if="appearNote.files.length > 0">
<MkMediaList :mediaList="appearNote.files"/>
</div>
@@ -237,6 +237,7 @@ import { checkWordMute } from '@/scripts/check-word-mute.js';
import { userPage } from '@/filters/user.js';
import { notePage } from '@/filters/note.js';
import * as os from '@/os.js';
+import * as sound from '@/scripts/sound.js';
import { defaultStore, noteViewInterruptors } from '@/store.js';
import { reactionPicker } from '@/scripts/reaction-picker.js';
import { extractUrlFromMfm } from '@/scripts/extract-url-from-mfm.js';
@@ -248,12 +249,11 @@ import { useNoteCapture } from '@/scripts/use-note-capture.js';
import { deepClone } from '@/scripts/clone.js';
import { useTooltip } from '@/scripts/use-tooltip.js';
import { claimAchievement } from '@/scripts/achievements.js';
-import { MenuItem } from '@/types/menu.js';
import { checkAnimationFromMfm } from '@/scripts/check-animated-mfm.js';
import MkRippleEffect from '@/components/MkRippleEffect.vue';
import { showMovedDialog } from '@/scripts/show-moved-dialog.js';
import MkUserCardMini from '@/components/MkUserCardMini.vue';
-import MkPagination, { Paging } from '@/components/MkPagination.vue';
+import MkPagination from '@/components/MkPagination.vue';
import MkReactionIcon from '@/components/MkReactionIcon.vue';
import MkButton from '@/components/MkButton.vue';
@@ -264,12 +264,12 @@ const props = defineProps<{
const inChannel = inject('inChannel', null);
-let note = $ref(deepClone(props.note));
+const note = ref(deepClone(props.note));
// plugin
if (noteViewInterruptors.length > 0) {
onMounted(async () => {
- let result: Misskey.entities.Note | null = deepClone(note);
+ let result: Misskey.entities.Note | null = deepClone(note.value);
for (const interruptor of noteViewInterruptors) {
try {
result = await interruptor.handler(result);
@@ -281,15 +281,15 @@ if (noteViewInterruptors.length > 0) {
console.error(err);
}
}
- note = result;
+ note.value = result;
});
}
const isRenote = (
- note.renote != null &&
- note.text == null &&
- note.fileIds.length === 0 &&
- note.poll == null
+ note.value.renote != null &&
+ note.value.text == null &&
+ note.value.fileIds.length === 0 &&
+ note.value.poll == null
);
const el = shallowRef<HTMLElement>();
@@ -301,26 +301,25 @@ const reactButton = shallowRef<HTMLElement>();
const quoteButton = shallowRef<HTMLElement>();
const clipButton = shallowRef<HTMLElement>();
const likeButton = shallowRef<HTMLElement>();
-let appearNote = $computed(() => isRenote ? note.renote as Misskey.entities.Note : note);
-const renoteUrl = appearNote.renote ? appearNote.renote.url : null;
-const renoteUri = appearNote.renote ? appearNote.renote.uri : null;
-
-const isMyRenote = $i && ($i.id === note.userId);
+const appearNote = computed(() => isRenote ? note.value.renote as Misskey.entities.Note : note.value);
+const renoteUrl = appearNote.value.renote ? appearNote.value.renote.url : null;
+const renoteUri = appearNote.value.renote ? appearNote.value.renote.uri : null;
+const isMyRenote = $i && ($i.id === note.value.userId);
const showContent = ref(defaultStore.state.uncollapseCW);
const isDeleted = ref(false);
const renoted = ref(false);
-const muted = ref($i ? checkWordMute(appearNote, $i, $i.mutedWords) : false);
+const muted = ref($i ? checkWordMute(appearNote.value, $i, $i.mutedWords) : false);
const translation = ref(null);
const translating = ref(false);
-const parsed = $computed(() => appearNote.text ? mfm.parse(appearNote.text) : null);
+const parsed = appearNote.value.text ? mfm.parse(appearNote.value.text) : null;
const urls = parsed ? extractUrlFromMfm(parsed).filter(u => u !== renoteUrl && u !== renoteUri) : null;
-const animated = $computed(() => parsed ? checkAnimationFromMfm(parsed) : null);
+const animated = computed(() => parsed ? checkAnimationFromMfm(parsed) : null);
const allowAnim = ref(defaultStore.state.advancedMfm && defaultStore.state.animatedMfm ? true : 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.value.user.instance);
const conversation = ref<Misskey.entities.Note[]>([]);
const replies = 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.value.visibility) || appearNote.value.userId === $i.id);
const defaultLike = computed(() => defaultStore.state.like ? defaultStore.state.like : null);
watch(() => props.expandAllCws, (expandAllCws) => {
@@ -328,8 +327,8 @@ watch(() => props.expandAllCws, (expandAllCws) => {
});
if ($i) {
- os.api("notes/renotes", {
- noteId: appearNote.id,
+ os.api('notes/renotes', {
+ noteId: appearNote.value.id,
userId: $i.id,
limit: 1,
}).then((res) => {
@@ -348,41 +347,41 @@ const keymap = {
provide('react', (reaction: string) => {
os.api('notes/reactions/create', {
- noteId: appearNote.id,
+ noteId: appearNote.value.id,
reaction: reaction,
});
});
-let tab = $ref('replies');
-let reactionTabType = $ref(null);
+const tab = ref('replies');
+const reactionTabType = ref(null);
-const renotesPagination = $computed(() => ({
+const renotesPagination = computed(() => ({
endpoint: 'notes/renotes',
limit: 10,
params: {
- noteId: appearNote.id,
+ noteId: appearNote.value.id,
},
}));
-const reactionsPagination = $computed(() => ({
+const reactionsPagination = computed(() => ({
endpoint: 'notes/reactions',
limit: 10,
params: {
- noteId: appearNote.id,
- type: reactionTabType,
+ noteId: appearNote.value.id,
+ type: reactionTabType.value,
},
}));
useNoteCapture({
rootEl: el,
- note: $$(appearNote),
- pureNote: $$(note),
+ note: appearNote,
+ pureNote: note,
isDeletedRef: isDeleted,
});
useTooltip(renoteButton, async (showing) => {
const renotes = await os.api('notes/renotes', {
- noteId: appearNote.id,
+ noteId: appearNote.value.id,
limit: 11,
});
@@ -393,14 +392,14 @@ useTooltip(renoteButton, async (showing) => {
os.popup(MkUsersTooltip, {
showing,
users,
- count: appearNote.renoteCount,
+ count: appearNote.value.renoteCount,
targetElement: renoteButton.value,
}, {}, 'closed');
});
useTooltip(quoteButton, async (showing) => {
const renotes = await os.api('notes/renotes', {
- noteId: appearNote.id,
+ noteId: appearNote.value.id,
limit: 11,
quote: true,
});
@@ -412,7 +411,7 @@ useTooltip(quoteButton, async (showing) => {
os.popup(MkUsersTooltip, {
showing,
users,
- count: appearNote.renoteCount,
+ count: appearNote.value.renoteCount,
targetElement: quoteButton.value,
}, {}, 'closed');
});
@@ -467,7 +466,7 @@ function renote(visibility: Visibility | 'local') {
pleaseLogin();
showMovedDialog();
- if (appearNote.channel) {
+ if (appearNote.value.channel) {
const el = renoteButton.value as HTMLElement | null | undefined;
if (el) {
const rect = el.getBoundingClientRect();
@@ -477,13 +476,13 @@ function renote(visibility: Visibility | 'local') {
}
os.api('notes/create', {
- renoteId: appearNote.id,
- channelId: appearNote.channelId,
+ renoteId: appearNote.value.id,
+ channelId: appearNote.value.channelId,
}).then(() => {
os.toast(i18n.ts.renoted);
renoted.value = true;
});
- } else if (!appearNote.channel || appearNote.channel?.allowRenoteToExternal) {
+ } else if (!appearNote.value.channel || appearNote.value.channel?.allowRenoteToExternal) {
const el = renoteButton.value as HTMLElement | null | undefined;
if (el) {
const rect = el.getBoundingClientRect();
@@ -495,15 +494,15 @@ function renote(visibility: Visibility | 'local') {
const configuredVisibility = defaultStore.state.rememberNoteVisibility ? defaultStore.state.visibility : defaultStore.state.defaultNoteVisibility;
const localOnlySetting = defaultStore.state.rememberNoteVisibility ? defaultStore.state.localOnly : defaultStore.state.defaultNoteLocalOnly;
- let noteVisibility = visibility === 'local' || visibility === 'specified' ? smallerVisibility(appearNote.visibility, configuredVisibility) : smallerVisibility(visibility, configuredVisibility);
- if (appearNote.channel?.isSensitive) {
- noteVisibility = smallerVisibility(visibility === 'local' || visibility === 'specified' ? appearNote.visibility : visibility, 'home');
+ let noteVisibility = visibility === 'local' || visibility === 'specified' ? smallerVisibility(appearNote.value.visibility, configuredVisibility) : smallerVisibility(visibility, configuredVisibility);
+ if (appearNote.value.channel?.isSensitive) {
+ noteVisibility = smallerVisibility(visibility === 'local' || visibility === 'specified' ? appearNote.value.visibility : visibility, 'home');
}
os.api('notes/create', {
localOnly: visibility === 'local' ? true : localOnlySetting,
visibility: noteVisibility,
- renoteId: appearNote.id,
+ renoteId: appearNote.value.id,
}).then(() => {
os.toast(i18n.ts.renoted);
renoted.value = true;
@@ -515,13 +514,13 @@ function quote() {
pleaseLogin();
showMovedDialog();
- if (appearNote.channel) {
+ if (appearNote.value.channel) {
os.post({
- renote: appearNote,
- channel: appearNote.channel,
+ renote: appearNote.value,
+ channel: appearNote.value.channel,
}).then(() => {
- os.api("notes/renotes", {
- noteId: appearNote.id,
+ os.api('notes/renotes', {
+ noteId: appearNote.value.id,
userId: $i.id,
limit: 1,
quote: true,
@@ -540,10 +539,10 @@ function quote() {
});
} else {
os.post({
- renote: appearNote,
+ renote: appearNote.value,
}).then(() => {
- os.api("notes/renotes", {
- noteId: appearNote.id,
+ os.api('notes/renotes', {
+ noteId: appearNote.value.id,
userId: $i.id,
limit: 1,
quote: true,
@@ -567,8 +566,8 @@ function reply(viaKeyboard = false): void {
pleaseLogin();
showMovedDialog();
os.post({
- reply: appearNote,
- channel: appearNote.channel,
+ reply: appearNote.value,
+ channel: appearNote.value.channel,
animation: !viaKeyboard,
}, () => {
focus();
@@ -578,9 +577,9 @@ function reply(viaKeyboard = false): void {
function react(viaKeyboard = false): void {
pleaseLogin();
showMovedDialog();
- if (appearNote.reactionAcceptance === 'likeOnly') {
+ if (appearNote.value.reactionAcceptance === 'likeOnly') {
os.api('notes/like', {
- noteId: appearNote.id,
+ noteId: appearNote.value.id,
override: defaultLike.value,
});
const el = reactButton.value as HTMLElement | null | undefined;
@@ -593,11 +592,13 @@ function react(viaKeyboard = false): void {
} else {
blur();
reactionPicker.show(reactButton.value, reaction => {
+ sound.play('reaction');
+
os.api('notes/reactions/create', {
- noteId: appearNote.id,
+ noteId: appearNote.value.id,
reaction: reaction,
});
- if (appearNote.text && appearNote.text.length > 100 && (Date.now() - new Date(appearNote.createdAt).getTime() < 1000 * 3)) {
+ if (appearNote.value.text && appearNote.value.text.length > 100 && (Date.now() - new Date(appearNote.value.createdAt).getTime() < 1000 * 3)) {
claimAchievement('reactWithoutRead');
}
}, () => {
@@ -610,7 +611,7 @@ function like(): void {
pleaseLogin();
showMovedDialog();
os.api('notes/like', {
- noteId: appearNote.id,
+ noteId: appearNote.value.id,
override: defaultLike.value,
});
const el = likeButton.value as HTMLElement | null | undefined;
@@ -632,8 +633,8 @@ function undoReact(note): void {
function undoRenote() : void {
if (!renoted.value) return;
- os.api("notes/unrenote", {
- noteId: appearNote.id,
+ os.api('notes/unrenote', {
+ noteId: appearNote.value.id,
});
os.toast(i18n.ts.rmboost);
renoted.value = false;
@@ -661,27 +662,27 @@ function onContextmenu(ev: MouseEvent): void {
ev.preventDefault();
react();
} else {
- const { menu, cleanup } = getNoteMenu({ note: note, translating, translation, menuButton, isDeleted });
+ const { menu, cleanup } = getNoteMenu({ note: note.value, translating, translation, menuButton, isDeleted });
os.contextMenu(menu, ev).then(focus).finally(cleanup);
}
}
function menu(viaKeyboard = false): void {
- const { menu, cleanup } = getNoteMenu({ note: note, translating, translation, menuButton, isDeleted });
+ const { menu, cleanup } = getNoteMenu({ note: note.value, translating, translation, menuButton, isDeleted });
os.popupMenu(menu, menuButton.value, {
viaKeyboard,
}).then(focus).finally(cleanup);
}
async function menuVersions(viaKeyboard = false): Promise<void> {
- const { menu, cleanup } = await getNoteVersionsMenu({ note: note, menuVersionsButton });
+ const { menu, cleanup } = await getNoteVersionsMenu({ note: note.value, menuVersionsButton });
os.popupMenu(menu, menuVersionsButton.value, {
viaKeyboard,
}).then(focus).finally(cleanup);
}
async function clip() {
- os.popupMenu(await getNoteClipMenu({ note: note, isDeleted }), clipButton.value).then(focus);
+ os.popupMenu(await getNoteClipMenu({ note: note.value, isDeleted }), clipButton.value).then(focus);
}
function showRenoteMenu(viaKeyboard = false): void {
@@ -693,7 +694,7 @@ function showRenoteMenu(viaKeyboard = false): void {
danger: true,
action: () => {
os.api('notes/delete', {
- noteId: note.id,
+ noteId: note.value.id,
});
isDeleted.value = true;
},
@@ -715,7 +716,7 @@ const repliesLoaded = ref(false);
function loadReplies() {
repliesLoaded.value = true;
os.api('notes/children', {
- noteId: appearNote.id,
+ noteId: appearNote.value.id,
limit: 30,
showQuotes: false,
}).then(res => {
@@ -730,7 +731,7 @@ const quotesLoaded = ref(false);
function loadQuotes() {
quotesLoaded.value = true;
os.api('notes/renotes', {
- noteId: appearNote.id,
+ noteId: appearNote.value.id,
limit: 30,
quote: true,
}).then(res => {
@@ -745,13 +746,13 @@ const conversationLoaded = ref(false);
function loadConversation() {
conversationLoaded.value = true;
os.api('notes/conversation', {
- noteId: appearNote.replyId,
+ noteId: appearNote.value.replyId,
}).then(res => {
conversation.value = res.reverse();
});
}
-if (appearNote.reply && appearNote.reply.replyId && defaultStore.state.autoloadConversation) loadConversation();
+if (appearNote.value.reply && appearNote.value.reply.replyId && defaultStore.state.autoloadConversation) loadConversation();
function animatedMFM() {
if (allowAnim.value) {
diff --git a/packages/frontend/src/components/MkNotePreview.vue b/packages/frontend/src/components/MkNotePreview.vue
index 552f8137ed..c517bc6800 100644
--- a/packages/frontend/src/components/MkNotePreview.vue
+++ b/packages/frontend/src/components/MkNotePreview.vue
@@ -11,7 +11,11 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkUserName :user="user" :nowrap="true"/>
</div>
<div>
- <div>
+ <p v-if="useCw" :class="$style.cw">
+ <Mfm v-if="cw != ''" :text="cw" :author="user" :nyaize="'respect'" :i="user" style="margin-right: 8px;"/>
+ <MkCwButton v-model="showContent" :text="text.trim()" :files="files" :poll="poll" style="margin: 4px 0;"/>
+ </p>
+ <div v-show="!useCw || showContent">
<Mfm :text="text.trim()" :author="user" :nyaize="'respect'" :i="user"/>
</div>
</div>
@@ -20,11 +24,23 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { } from 'vue';
+import { ref } from 'vue';
import * as Misskey from 'misskey-js';
+import MkCwButton from '@/components/MkCwButton.vue';
+
+const showContent = ref(false);
const props = defineProps<{
text: string;
+ files: Misskey.entities.DriveFile[];
+ poll?: {
+ choices: string[];
+ multiple: boolean;
+ expiresAt: string | null;
+ expiredAfter: string | null;
+ };
+ useCw: boolean;
+ cw: string | null;
user: Misskey.entities.User;
}>();
</script>
@@ -53,6 +69,14 @@ const props = defineProps<{
min-width: 0;
}
+.cw {
+ cursor: default;
+ display: block;
+ margin: 0;
+ padding: 0;
+ overflow-wrap: break-word;
+}
+
.header {
margin-bottom: 2px;
font-weight: bold;
diff --git a/packages/frontend/src/components/MkNoteSimple.vue b/packages/frontend/src/components/MkNoteSimple.vue
index bc0f82d44d..7a6109ee0b 100644
--- a/packages/frontend/src/components/MkNoteSimple.vue
+++ b/packages/frontend/src/components/MkNoteSimple.vue
@@ -11,7 +11,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<div>
<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"/>
- <MkCwButton v-model="showContent" :note="note" v-on:click.stop/>
+ <MkCwButton v-model="showContent" :text="note.text" :files="note.files" :poll="note.poll" @click.stop/>
</p>
<div v-show="note.cw == null || showContent">
<MkSubNoteContent :hideFiles="hideFiles" :class="$style.text" :note="note"/>
@@ -22,12 +22,11 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { watch } from 'vue';
+import { ref, watch } from 'vue';
import * as Misskey from 'misskey-js';
import MkNoteHeader from '@/components/MkNoteHeader.vue';
import MkSubNoteContent from '@/components/MkSubNoteContent.vue';
import MkCwButton from '@/components/MkCwButton.vue';
-import { $i } from '@/account.js';
import { defaultStore } from '@/store.js';
const props = defineProps<{
@@ -36,10 +35,10 @@ const props = defineProps<{
hideFiles?: boolean;
}>();
-let showContent = $ref(defaultStore.state.uncollapseCW);
+let showContent = ref(defaultStore.state.uncollapseCW);
watch(() => props.expandAllCws, (expandAllCws) => {
- if (expandAllCws !== showContent) showContent = expandAllCws;
+ if (expandAllCws !== showContent.value) showContent.value = expandAllCws;
});
</script>
diff --git a/packages/frontend/src/components/MkNoteSub.vue b/packages/frontend/src/components/MkNoteSub.vue
index 5b1e1af308..8d394c0c15 100644
--- a/packages/frontend/src/components/MkNoteSub.vue
+++ b/packages/frontend/src/components/MkNoteSub.vue
@@ -13,7 +13,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<div :class="$style.content">
<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'"/>
- <MkCwButton v-model="showContent" :note="note"/>
+ <MkCwButton v-model="showContent" :text="note.text" :files="note.files" :poll="note.poll"/>
</p>
<div v-show="note.cw == null || showContent">
<MkSubNoteContent :class="$style.text" :note="note" :translating="translating" :translation="translation"/>
@@ -93,15 +93,14 @@ import { notePage } from '@/filters/note.js';
import * as os from '@/os.js';
import { i18n } from '@/i18n.js';
import { $i } from '@/account.js';
-import { userPage } from "@/filters/user.js";
-import { checkWordMute } from "@/scripts/check-word-mute.js";
-import { defaultStore } from "@/store.js";
+import { userPage } from '@/filters/user.js';
+import { checkWordMute } from '@/scripts/check-word-mute.js';
+import { defaultStore } from '@/store.js';
import { pleaseLogin } from '@/scripts/please-login.js';
import { showMovedDialog } from '@/scripts/show-moved-dialog.js';
import MkRippleEffect from '@/components/MkRippleEffect.vue';
import { reactionPicker } from '@/scripts/reaction-picker.js';
import { claimAchievement } from '@/scripts/achievements.js';
-import type { MenuItem } from '@/types/menu.js';
import { getNoteMenu } from '@/scripts/get-note-menu.js';
import { useNoteCapture } from '@/scripts/use-note-capture.js';
@@ -131,7 +130,7 @@ const quoteButton = shallowRef<HTMLElement>();
const menuButton = 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 isRenote = (
@@ -143,13 +142,13 @@ const isRenote = (
useNoteCapture({
rootEl: el,
- note: $$(appearNote),
+ note: appearNote,
isDeletedRef: isDeleted,
});
if ($i) {
- os.api("notes/renotes", {
- noteId: appearNote.id,
+ os.api('notes/renotes', {
+ noteId: appearNote.value.id,
userId: $i.id,
limit: 1,
}).then((res) => {
@@ -230,8 +229,8 @@ function undoReact(note): void {
function undoRenote() : void {
if (!renoted.value) return;
- os.api("notes/unrenote", {
- noteId: appearNote.id,
+ os.api('notes/unrenote', {
+ noteId: appearNote.value.id,
});
os.toast(i18n.ts.rmboost);
renoted.value = false;
@@ -245,13 +244,13 @@ function undoRenote() : void {
}
}
-let showContent = $ref(defaultStore.state.uncollapseCW);
+let showContent = ref(defaultStore.state.uncollapseCW);
watch(() => props.expandAllCws, (expandAllCws) => {
- if (expandAllCws !== showContent) showContent = expandAllCws;
+ if (expandAllCws !== showContent.value) showContent.value = expandAllCws;
});
-let replies: Misskey.entities.Note[] = $ref([]);
+let replies = ref<Misskey.entities.Note[]>([]);
function boostVisibility() {
os.popupMenu([
@@ -293,7 +292,7 @@ function renote(visibility: 'public' | 'home' | 'followers' | 'specified' | 'loc
pleaseLogin();
showMovedDialog();
- if (appearNote.channel) {
+ if (appearNote.value.channel) {
const el = renoteButton.value as HTMLElement | null | undefined;
if (el) {
const rect = el.getBoundingClientRect();
@@ -333,12 +332,12 @@ function quote() {
pleaseLogin();
showMovedDialog();
- if (appearNote.channel) {
+ if (appearNote.value.channel) {
os.post({
- renote: appearNote,
- channel: appearNote.channel,
+ renote: appearNote.value,
+ channel: appearNote.value.channel,
}).then(() => {
- os.api("notes/renotes", {
+ os.api('notes/renotes', {
noteId: props.note.id,
userId: $i.id,
limit: 1,
@@ -358,9 +357,9 @@ function quote() {
});
} else {
os.post({
- renote: appearNote,
+ renote: appearNote.value,
}).then(() => {
- os.api("notes/renotes", {
+ os.api('notes/renotes', {
noteId: props.note.id,
userId: $i.id,
limit: 1,
@@ -394,7 +393,7 @@ if (props.detail) {
limit: numberOfReplies.value,
showQuotes: false,
}).then(res => {
- replies = res;
+ replies.value = res;
});
}
</script>
diff --git a/packages/frontend/src/components/MkNotes.vue b/packages/frontend/src/components/MkNotes.vue
index 0d2f0020d1..fc1c8a0f09 100644
--- a/packages/frontend/src/components/MkNotes.vue
+++ b/packages/frontend/src/components/MkNotes.vue
@@ -25,7 +25,7 @@ SPDX-License-Identifier: AGPL-3.0-only
:ad="true"
:class="$style.notes"
>
- <MkNote :key="note._featuredId_ || note._prId_ || note.id" :class="$style.note" :note="note"/>
+ <MkNote :key="note._featuredId_ || note._prId_ || note.id" :class="$style.note" :note="note" :withHardMute="true"/>
</MkDateSeparatedList>
<MkDateSeparatedList
v-else-if="defaultStore.state.noteDesign === 'sharkey'"
diff --git a/packages/frontend/src/components/MkNotification.vue b/packages/frontend/src/components/MkNotification.vue
index ae5be0f2d4..2901139220 100644
--- a/packages/frontend/src/components/MkNotification.vue
+++ b/packages/frontend/src/components/MkNotification.vue
@@ -8,6 +8,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<div :class="$style.head">
<MkAvatar v-if="notification.type === 'pollEnded'" :class="$style.icon" :user="notification.note.user" link preview/>
<MkAvatar v-else-if="notification.type === 'note'" :class="$style.icon" :user="notification.note.user" link preview/>
+ <MkAvatar v-else-if="notification.type === 'roleAssigned'" :class="$style.icon" :user="$i" link preview/>
<MkAvatar v-else-if="notification.type === 'achievementEarned'" :class="$style.icon" :user="$i" link preview/>
<div v-else-if="notification.type === 'reaction:grouped'" :class="[$style.icon, $style.icon_reactionGroup]"><i class="ph-smiley ph-bold ph-lg" style="line-height: 1;"></i></div>
<div v-else-if="notification.type === 'renote:grouped'" :class="[$style.icon, $style.icon_renoteGroup]"><i class="ph-rocket-launch ph-bold ph-lg" style="line-height: 1;"></i></div>
@@ -36,6 +37,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<i v-else-if="notification.type === 'quote'" class="ph-quotes ph-bold ph-lg"></i>
<i v-else-if="notification.type === 'pollEnded'" class="ph-chart-bar-horizontal ph-bold ph-lg"></i>
<i v-else-if="notification.type === 'achievementEarned'" class="ph-trophy ph-bold ph-lg"></i>
+ <img v-else-if="notification.type === 'roleAssigned'" :src="notification.role.iconUrl" alt=""/>
<!-- notification.reaction が null になることはまずないが、ここでoptional chaining使うと一部ブラウザで刺さるので念の為 -->
<MkReactionIcon
v-else-if="notification.type === 'reaction'"
@@ -50,6 +52,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<header :class="$style.header">
<span v-if="notification.type === 'pollEnded'">{{ i18n.ts._notification.pollEnded }}</span>
<span v-else-if="notification.type === 'note'">{{ i18n.ts._notification.newNote }}: <MkUserName :user="notification.note.user"/></span>
+ <span v-else-if="notification.type === 'roleAssigned'">{{ i18n.ts._notification.roleAssigned }}</span>
<span v-else-if="notification.type === 'achievementEarned'">{{ i18n.ts._notification.achievementEarned }}</span>
<span v-else-if="notification.type === 'test'">{{ i18n.ts._notification.testNotification }}</span>
<MkA v-else-if="notification.user" v-user-preview="notification.user.id" :class="$style.headerName" :to="userPage(notification.user)"><MkUserName :user="notification.user"/></MkA>
@@ -86,6 +89,9 @@ SPDX-License-Identifier: AGPL-3.0-only
<Mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="true" :author="notification.note.user"/>
<i class="ph-quotes ph-bold ph-lg" :class="$style.quote"></i>
</MkA>
+ <div v-else-if="notification.type === 'roleAssigned'" :class="$style.text">
+ {{ notification.role.name }}
+ </div>
<MkA v-else-if="notification.type === 'achievementEarned'" :class="$style.text" to="/my/achievements">
{{ i18n.ts._achievements._types['_' + notification.achievement].title }}
</MkA>
@@ -130,7 +136,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { ref, shallowRef } from 'vue';
+import { ref } from 'vue';
import * as Misskey from 'misskey-js';
import MkReactionIcon from '@/components/MkReactionIcon.vue';
import MkFollowButton from '@/components/MkFollowButton.vue';
diff --git a/packages/frontend/src/components/MkNotificationSelectWindow.vue b/packages/frontend/src/components/MkNotificationSelectWindow.vue
index 3d5a56975b..6725776f43 100644
--- a/packages/frontend/src/components/MkNotificationSelectWindow.vue
+++ b/packages/frontend/src/components/MkNotificationSelectWindow.vue
@@ -30,7 +30,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { ref, Ref } from 'vue';
+import { ref, Ref, shallowRef } from 'vue';
import MkSwitch from './MkSwitch.vue';
import MkInfo from './MkInfo.vue';
import MkButton from './MkButton.vue';
@@ -51,7 +51,7 @@ const props = withDefaults(defineProps<{
excludeTypes: () => [],
});
-const dialog = $shallowRef<InstanceType<typeof MkModalWindow>>();
+const dialog = shallowRef<InstanceType<typeof MkModalWindow>>();
const typesMap: TypesMap = notificationTypes.reduce((p, t) => ({ ...p, [t]: ref<boolean>(!props.excludeTypes.includes(t)) }), {} as any);
@@ -61,7 +61,7 @@ function ok() {
.filter(type => !typesMap[type].value),
});
- if (dialog) dialog.close();
+ if (dialog.value) dialog.value.close();
}
function disableAll() {
diff --git a/packages/frontend/src/components/MkNotifications.vue b/packages/frontend/src/components/MkNotifications.vue
index bfe668a165..a157820d56 100644
--- a/packages/frontend/src/components/MkNotifications.vue
+++ b/packages/frontend/src/components/MkNotifications.vue
@@ -15,11 +15,11 @@ SPDX-License-Identifier: AGPL-3.0-only
<template #default="{ items: notifications }">
<MkDateSeparatedList v-if="defaultStore.state.noteDesign === 'misskey'" v-slot="{ item: notification }" :class="$style.list" :items="notifications" :noGap="true">
- <MkNote v-if="['reply', 'quote', 'mention'].includes(notification.type)" :key="notification.id" :note="notification.note"/>
+ <MkNote v-if="['reply', 'quote', 'mention'].includes(notification.type)" :key="notification.id" :note="notification.note" :withHardMute="true"/>
<XNotification v-else :key="notification.id" :notification="notification" :withTime="true" :full="true" class="_panel"/>
</MkDateSeparatedList>
<MkDateSeparatedList v-else-if="defaultStore.state.noteDesign === 'sharkey'" v-slot="{ item: notification }" :class="$style.list" :items="notifications" :noGap="true">
- <SkNote v-if="['reply', 'quote', 'mention'].includes(notification.type)" :key="notification.id" :note="notification.note"/>
+ <SkNote v-if="['reply', 'quote', 'mention'].includes(notification.type)" :key="notification.id" :note="notification.note" :withHardMute="true"/>
<XNotification v-else :key="notification.id" :notification="notification" :withTime="true" :full="true" class="_panel"/>
</MkDateSeparatedList>
</template>
@@ -29,13 +29,12 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { onUnmounted, onDeactivated, onMounted, computed, shallowRef, onActivated } from 'vue';
-import MkPagination, { Paging } from '@/components/MkPagination.vue';
+import MkPagination from '@/components/MkPagination.vue';
import XNotification from '@/components/MkNotification.vue';
import MkDateSeparatedList from '@/components/MkDateSeparatedList.vue';
import MkNote from '@/components/MkNote.vue';
import SkNote from '@/components/SkNote.vue';
import { useStream } from '@/stream.js';
-import { $i } from '@/account.js';
import { i18n } from '@/i18n.js';
import { notificationTypes } from '@/const.js';
import { infoImageUrl } from '@/instance.js';
@@ -48,7 +47,7 @@ const props = defineProps<{
const pagingComponent = shallowRef<InstanceType<typeof MkPagination>>();
-const pagination: Paging = defaultStore.state.useGroupedNotifications ? {
+const pagination = computed(() => defaultStore.reactiveState.useGroupedNotifications.value ? {
endpoint: 'i/notifications-grouped' as const,
limit: 20,
params: computed(() => ({
@@ -60,7 +59,7 @@ const pagination: Paging = defaultStore.state.useGroupedNotifications ? {
params: computed(() => ({
excludeTypes: props.excludeTypes ?? undefined,
})),
-};
+});
function onNotification(notification) {
const isMuted = props.excludeTypes ? props.excludeTypes.includes(notification.type) : false;
diff --git a/packages/frontend/src/components/MkOmit.vue b/packages/frontend/src/components/MkOmit.vue
index ac957d93dc..702bb95dc7 100644
--- a/packages/frontend/src/components/MkOmit.vue
+++ b/packages/frontend/src/components/MkOmit.vue
@@ -13,7 +13,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { onMounted, onUnmounted } from 'vue';
+import { onMounted, onUnmounted, shallowRef, ref } from 'vue';
import { i18n } from '@/i18n.js';
const props = withDefaults(defineProps<{
@@ -22,13 +22,13 @@ const props = withDefaults(defineProps<{
maxHeight: 200,
});
-let content = $shallowRef<HTMLElement>();
-let omitted = $ref(false);
-let ignoreOmit = $ref(false);
+const content = shallowRef<HTMLElement>();
+const omitted = ref(false);
+const ignoreOmit = ref(false);
const calcOmit = () => {
- if (omitted || ignoreOmit) return;
- omitted = content.offsetHeight > props.maxHeight;
+ if (omitted.value || ignoreOmit.value) return;
+ omitted.value = content.value.offsetHeight > props.maxHeight;
};
const omitObserver = new ResizeObserver((entries, observer) => {
@@ -37,7 +37,7 @@ const omitObserver = new ResizeObserver((entries, observer) => {
onMounted(() => {
calcOmit();
- omitObserver.observe(content);
+ omitObserver.observe(content.value);
});
onUnmounted(() => {
diff --git a/packages/frontend/src/components/MkPagePreview.vue b/packages/frontend/src/components/MkPagePreview.vue
index 05b577c49c..6c8a0e56a6 100644
--- a/packages/frontend/src/components/MkPagePreview.vue
+++ b/packages/frontend/src/components/MkPagePreview.vue
@@ -114,7 +114,6 @@ const props = defineProps<{
& + article {
left: 0;
- width: 100%;
}
}
}
@@ -124,6 +123,7 @@ const props = defineProps<{
> .thumbnail {
height: 80px;
+ overflow: clip;
}
> article {
diff --git a/packages/frontend/src/components/MkPageWindow.vue b/packages/frontend/src/components/MkPageWindow.vue
index 163cba5e3c..d1d4c2106c 100644
--- a/packages/frontend/src/components/MkPageWindow.vue
+++ b/packages/frontend/src/components/MkPageWindow.vue
@@ -29,7 +29,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { ComputedRef, onMounted, onUnmounted, provide, shallowRef } from 'vue';
+import { ComputedRef, onMounted, onUnmounted, provide, shallowRef, ref, computed } from 'vue';
import RouterView from '@/components/global/RouterView.vue';
import MkWindow from '@/components/MkWindow.vue';
import { popout as _popout } from '@/scripts/popout.js';
@@ -55,16 +55,16 @@ defineEmits<{
const router = new Router(routes, props.initialPath, !!$i, page(() => import('@/pages/not-found.vue')));
const contents = shallowRef<HTMLElement>();
-let pageMetadata = $ref<null | ComputedRef<PageMetadata>>();
-let windowEl = $shallowRef<InstanceType<typeof MkWindow>>();
-const history = $ref<{ path: string; key: any; }[]>([{
+const pageMetadata = ref<null | ComputedRef<PageMetadata>>();
+const windowEl = shallowRef<InstanceType<typeof MkWindow>>();
+const history = ref<{ path: string; key: any; }[]>([{
path: router.getCurrentPath(),
key: router.getCurrentKey(),
}]);
-const buttonsLeft = $computed(() => {
+const buttonsLeft = computed(() => {
const buttons = [];
- if (history.length > 1) {
+ if (history.value.length > 1) {
buttons.push({
icon: 'ph-arrow-left ph-bold ph-lg',
onClick: back,
@@ -73,7 +73,7 @@ const buttonsLeft = $computed(() => {
return buttons;
});
-const buttonsRight = $computed(() => {
+const buttonsRight = computed(() => {
const buttons = [{
icon: 'ph-arrow-clockwise ph-bold ph-lg',
title: i18n.ts.reload,
@@ -86,22 +86,22 @@ const buttonsRight = $computed(() => {
return buttons;
});
-let reloadCount = $ref(0);
+const reloadCount = ref(0);
router.addListener('push', ctx => {
- history.push({ path: ctx.path, key: ctx.key });
+ history.value.push({ path: ctx.path, key: ctx.key });
});
provide('router', router);
provideMetadataReceiver((info) => {
- pageMetadata = info;
+ pageMetadata.value = info;
});
provide('shouldOmitHeaderTitle', true);
provide('shouldHeaderThin', true);
provide('forceSpacerMin', true);
provide('shouldBackButton', false);
-const contextmenu = $computed(() => ([{
+const contextmenu = computed(() => ([{
icon: 'ph-eject ph-bold ph-lg',
text: i18n.ts.showInPage,
action: expand,
@@ -113,8 +113,8 @@ const contextmenu = $computed(() => ([{
icon: 'ph-arrow-square-out ph-bold ph-lg',
text: i18n.ts.openInNewTab,
action: () => {
- window.open(url + router.getCurrentPath(), '_blank');
- windowEl.close();
+ window.open(url + router.getCurrentPath(), '_blank', 'noopener');
+ windowEl.value.close();
},
}, {
icon: 'ph-link ph-bold ph-lg',
@@ -125,26 +125,26 @@ const contextmenu = $computed(() => ([{
}]));
function back() {
- history.pop();
- router.replace(history.at(-1)!.path, history.at(-1)!.key);
+ history.value.pop();
+ router.replace(history.value.at(-1)!.path, history.value.at(-1)!.key);
}
function reload() {
- reloadCount++;
+ reloadCount.value++;
}
function close() {
- windowEl.close();
+ windowEl.value.close();
}
function expand() {
mainRouter.push(router.getCurrentPath(), 'forcePage');
- windowEl.close();
+ windowEl.value.close();
}
function popout() {
- _popout(router.getCurrentPath(), windowEl.$el);
- windowEl.close();
+ _popout(router.getCurrentPath(), windowEl.value.$el);
+ windowEl.value.close();
}
useScrollPositionManager(() => getScrollContainer(contents.value), router);
diff --git a/packages/frontend/src/components/MkPagination.vue b/packages/frontend/src/components/MkPagination.vue
index e7796dfcb5..07347eda29 100644
--- a/packages/frontend/src/components/MkPagination.vue
+++ b/packages/frontend/src/components/MkPagination.vue
@@ -43,7 +43,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts">
-import { computed, ComputedRef, isRef, nextTick, onActivated, onBeforeMount, onBeforeUnmount, onDeactivated, ref, watch } from 'vue';
+import { computed, ComputedRef, isRef, nextTick, onActivated, onBeforeMount, onBeforeUnmount, onDeactivated, ref, shallowRef, watch } from 'vue';
import * as Misskey from 'misskey-js';
import * as os from '@/os.js';
import { onScrollTop, isTopVisible, getBodyScrollHeight, getScrollContainer, onScrollBottom, scrollToBottom, scroll, isBottomVisible } from '@/scripts/scroll.js';
@@ -105,12 +105,12 @@ const emit = defineEmits<{
(ev: 'status', error: boolean): void;
}>();
-let rootEl = $shallowRef<HTMLElement>();
+const rootEl = shallowRef<HTMLElement>();
// 遡り中かどうか
-let backed = $ref(false);
+const backed = ref(false);
-let scrollRemove = $ref<(() => void) | null>(null);
+const scrollRemove = ref<(() => void) | null>(null);
/**
* 表示するアイテムのソース
@@ -142,8 +142,8 @@ const {
enableInfiniteScroll,
} = defaultStore.reactiveState;
-const contentEl = $computed(() => props.pagination.pageEl ?? rootEl);
-const scrollableElement = $computed(() => contentEl ? getScrollContainer(contentEl) : document.body);
+const contentEl = computed(() => props.pagination.pageEl ?? rootEl.value);
+const scrollableElement = computed(() => contentEl.value ? getScrollContainer(contentEl.value) : document.body);
const visibility = useDocumentVisibility();
@@ -153,40 +153,40 @@ const BACKGROUND_PAUSE_WAIT_SEC = 10;
// 先頭が表示されているかどうかを検出
// https://qiita.com/mkataigi/items/0154aefd2223ce23398e
-let scrollObserver = $ref<IntersectionObserver>();
+const scrollObserver = ref<IntersectionObserver>();
-watch([() => props.pagination.reversed, $$(scrollableElement)], () => {
- if (scrollObserver) scrollObserver.disconnect();
+watch([() => props.pagination.reversed, scrollableElement], () => {
+ if (scrollObserver.value) scrollObserver.value.disconnect();
- scrollObserver = new IntersectionObserver(entries => {
- backed = entries[0].isIntersecting;
+ scrollObserver.value = new IntersectionObserver(entries => {
+ backed.value = entries[0].isIntersecting;
}, {
- root: scrollableElement,
+ root: scrollableElement.value,
rootMargin: props.pagination.reversed ? '-100% 0px 100% 0px' : '100% 0px -100% 0px',
threshold: 0.01,
});
}, { immediate: true });
-watch($$(rootEl), () => {
- scrollObserver?.disconnect();
+watch(rootEl, () => {
+ scrollObserver.value?.disconnect();
nextTick(() => {
- if (rootEl) scrollObserver?.observe(rootEl);
+ if (rootEl.value) scrollObserver.value?.observe(rootEl.value);
});
});
-watch([$$(backed), $$(contentEl)], () => {
- if (!backed) {
- if (!contentEl) return;
+watch([backed, contentEl], () => {
+ if (!backed.value) {
+ if (!contentEl.value) return;
- scrollRemove = (props.pagination.reversed ? onScrollBottom : onScrollTop)(contentEl, executeQueue, TOLERANCE);
+ scrollRemove.value = (props.pagination.reversed ? onScrollBottom : onScrollTop)(contentEl.value, executeQueue, TOLERANCE);
} else {
- if (scrollRemove) scrollRemove();
- scrollRemove = null;
+ if (scrollRemove.value) scrollRemove.value();
+ scrollRemove.value = null;
}
});
// パラメータに何らかの変更があった際、再読込したい(チャンネル等のIDが変わったなど)
-watch(() => props.pagination.params, init, { deep: true });
+watch(() => [props.pagination.endpoint, props.pagination.params], init, { deep: true });
watch(queue, (a, b) => {
if (a.size === 0 && b.size === 0) return;
@@ -206,6 +206,7 @@ async function init(): Promise<void> {
await os.api(props.pagination.endpoint, {
...params,
limit: props.pagination.limit ?? 10,
+ allowPartial: true,
}).then(res => {
for (let i = 0; i < res.length; i++) {
const item = res[i];
@@ -253,14 +254,14 @@ const fetchMore = async (): Promise<void> => {
}
const reverseConcat = _res => {
- const oldHeight = scrollableElement ? scrollableElement.scrollHeight : getBodyScrollHeight();
- const oldScroll = scrollableElement ? scrollableElement.scrollTop : window.scrollY;
+ const oldHeight = scrollableElement.value ? scrollableElement.value.scrollHeight : getBodyScrollHeight();
+ const oldScroll = scrollableElement.value ? scrollableElement.value.scrollTop : window.scrollY;
items.value = concatMapWithArray(items.value, _res);
return nextTick(() => {
- if (scrollableElement) {
- scroll(scrollableElement, { top: oldScroll + (scrollableElement.scrollHeight - oldHeight), behavior: 'instant' });
+ if (scrollableElement.value) {
+ scroll(scrollableElement.value, { top: oldScroll + (scrollableElement.value.scrollHeight - oldHeight), behavior: 'instant' });
} else {
window.scroll({ top: oldScroll + (getBodyScrollHeight() - oldHeight), behavior: 'instant' });
}
@@ -350,7 +351,7 @@ const appearFetchMoreAhead = async (): Promise<void> => {
fetchMoreAppearTimeout();
};
-const isTop = (): boolean => isBackTop.value || (props.pagination.reversed ? isBottomVisible : isTopVisible)(contentEl!, TOLERANCE);
+const isTop = (): boolean => isBackTop.value || (props.pagination.reversed ? isBottomVisible : isTopVisible)(contentEl.value!, TOLERANCE);
watch(visibility, () => {
if (visibility.value === 'hidden') {
@@ -444,11 +445,11 @@ onActivated(() => {
});
onDeactivated(() => {
- isBackTop.value = props.pagination.reversed ? window.scrollY >= (rootEl ? rootEl.scrollHeight - window.innerHeight : 0) : window.scrollY === 0;
+ isBackTop.value = props.pagination.reversed ? window.scrollY >= (rootEl.value ? rootEl.value.scrollHeight - window.innerHeight : 0) : window.scrollY === 0;
});
function toBottom() {
- scrollToBottom(contentEl!);
+ scrollToBottom(contentEl.value!);
}
onBeforeMount(() => {
@@ -476,13 +477,13 @@ onBeforeUnmount(() => {
clearTimeout(preventAppearFetchMoreTimer.value);
preventAppearFetchMoreTimer.value = null;
}
- scrollObserver?.disconnect();
+ scrollObserver.value?.disconnect();
});
defineExpose({
items,
queue,
- backed,
+ backed: backed.value,
more,
reload,
prepend,
diff --git a/packages/frontend/src/components/MkPasswordDialog.vue b/packages/frontend/src/components/MkPasswordDialog.vue
index 3f244c42fd..711c54c7f1 100644
--- a/packages/frontend/src/components/MkPasswordDialog.vue
+++ b/packages/frontend/src/components/MkPasswordDialog.vue
@@ -36,7 +36,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { onMounted } from 'vue';
+import { onMounted, shallowRef, ref } from 'vue';
import MkInput from '@/components/MkInput.vue';
import MkButton from '@/components/MkButton.vue';
import MkModalWindow from '@/components/MkModalWindow.vue';
@@ -49,22 +49,22 @@ const emit = defineEmits<{
(ev: 'cancelled'): void;
}>();
-const dialog = $shallowRef<InstanceType<typeof MkModalWindow>>();
-const passwordInput = $shallowRef<InstanceType<typeof MkInput>>();
-const password = $ref('');
-const token = $ref(null);
+const dialog = shallowRef<InstanceType<typeof MkModalWindow>>();
+const passwordInput = shallowRef<InstanceType<typeof MkInput>>();
+const password = ref('');
+const token = ref(null);
function onClose() {
emit('cancelled');
- if (dialog) dialog.close();
+ if (dialog.value) dialog.value.close();
}
function done(res) {
- emit('done', { password, token });
- if (dialog) dialog.close();
+ emit('done', { password: password.value, token: token.value });
+ if (dialog.value) dialog.value.close();
}
onMounted(() => {
- if (passwordInput) passwordInput.focus();
+ if (passwordInput.value) passwordInput.value.focus();
});
</script>
diff --git a/packages/frontend/src/components/MkPlusOneEffect.vue b/packages/frontend/src/components/MkPlusOneEffect.vue
index 0bc98f4334..a741a3f7a8 100644
--- a/packages/frontend/src/components/MkPlusOneEffect.vue
+++ b/packages/frontend/src/components/MkPlusOneEffect.vue
@@ -10,7 +10,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { onMounted } from 'vue';
+import { onMounted, ref } from 'vue';
import * as os from '@/os.js';
const props = withDefaults(defineProps<{
@@ -23,13 +23,13 @@ const emit = defineEmits<{
(ev: 'end'): void;
}>();
-let up = $ref(false);
+const up = ref(false);
const zIndex = os.claimZIndex('middle');
const angle = (45 - (Math.random() * 90)) + 'deg';
onMounted(() => {
window.setTimeout(() => {
- up = true;
+ up.value = true;
}, 10);
window.setTimeout(() => {
diff --git a/packages/frontend/src/components/MkPopupMenu.vue b/packages/frontend/src/components/MkPopupMenu.vue
index 146b9d7ccf..1d92374f4f 100644
--- a/packages/frontend/src/components/MkPopupMenu.vue
+++ b/packages/frontend/src/components/MkPopupMenu.vue
@@ -10,10 +10,10 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { ref } from 'vue';
+import { ref, shallowRef } from 'vue';
import MkModal from './MkModal.vue';
import MkMenu from './MkMenu.vue';
-import { MenuItem } from '@/types/menu';
+import { MenuItem } from '@/types/menu.js';
defineProps<{
items: MenuItem[];
@@ -28,7 +28,7 @@ const emit = defineEmits<{
(ev: 'closing'): void;
}>();
-let modal = $shallowRef<InstanceType<typeof MkModal>>();
+const modal = shallowRef<InstanceType<typeof MkModal>>();
const manualShowing = ref(true);
const hiding = ref(false);
@@ -60,14 +60,14 @@ function hide() {
hiding.value = true;
// closeは呼ぶ必要がある
- modal?.close();
+ modal.value?.close();
}
function close() {
manualShowing.value = false;
// closeは呼ぶ必要がある
- modal?.close();
+ modal.value?.close();
}
</script>
diff --git a/packages/frontend/src/components/MkPostForm.vue b/packages/frontend/src/components/MkPostForm.vue
index 5c9ac40427..c9784fc40f 100644
--- a/packages/frontend/src/components/MkPostForm.vue
+++ b/packages/frontend/src/components/MkPostForm.vue
@@ -67,13 +67,14 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkInfo v-if="hasNotSpecifiedMentions" warn :class="$style.hasNotSpecifiedMentions">{{ i18n.ts.notSpecifiedMentionWarning }} - <button class="_textButton" @click="addMissingMention()">{{ i18n.ts.add }}</button></MkInfo>
<input v-show="useCw" ref="cwInputEl" v-model="cw" :class="$style.cw" :placeholder="i18n.ts.annotation" @keydown="onKeydown">
<div :class="[$style.textOuter, { [$style.withCw]: useCw }]">
- <textarea ref="textareaEl" v-model="text" :class="[$style.text]" :disabled="posting || posted" :placeholder="placeholder" data-cy-post-form-text @keydown="onKeydown" @paste="onPaste" @compositionupdate="onCompositionUpdate" @compositionend="onCompositionEnd"/>
+ <div v-if="channel" :class="$style.colorBar" :style="{ background: channel.color }"></div>
+ <textarea ref="textareaEl" v-model="text" :class="[$style.text]" :disabled="posting || posted" :readonly="textAreaReadOnly" :placeholder="placeholder" data-cy-post-form-text @keydown="onKeydown" @paste="onPaste" @compositionupdate="onCompositionUpdate" @compositionend="onCompositionEnd"/>
<div v-if="maxTextLength - textLength < 100" :class="['_acrylic', $style.textCount, { [$style.textOver]: textLength > maxTextLength }]">{{ maxTextLength - textLength }}</div>
</div>
<input v-show="withHashtags" ref="hashtagsInputEl" v-model="hashtags" :class="$style.hashtags" :placeholder="i18n.ts.hashtags" list="hashtags">
<XPostFormAttaches v-model="files" @detach="detachFile" @changeSensitive="updateFileSensitive" @changeName="updateFileName" @replaceFile="replaceFile"/>
<MkPollEditor v-if="poll" v-model="poll" @destroyed="poll = null"/>
- <MkNotePreview v-if="showPreview" :class="$style.preview" :text="text" :user="postAccount ?? $i"/>
+ <MkNotePreview v-if="showPreview" :class="$style.preview" :text="text" :files="files" :poll="poll ?? undefined" :useCw="useCw" :cw="cw" :user="postAccount ?? $i"/>
<div v-if="showingOptions" style="padding: 8px 16px;">
</div>
<footer :class="$style.footer">
@@ -99,7 +100,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { inject, watch, nextTick, onMounted, defineAsyncComponent, provide } from 'vue';
+import { inject, watch, nextTick, onMounted, defineAsyncComponent, provide, shallowRef, ref, computed } from 'vue';
import * as mfm from '@sharkey/sfm-js';
import * as Misskey from 'misskey-js';
import insertTextAtCursor from 'insert-text-at-cursor';
@@ -125,6 +126,7 @@ import { deepClone } from '@/scripts/clone.js';
import MkRippleEffect from '@/components/MkRippleEffect.vue';
import { miLocalStorage } from '@/local-storage.js';
import { claimAchievement } from '@/scripts/achievements.js';
+import { emojiPicker } from '@/scripts/emoji-picker.js';
const modal = inject('modal');
@@ -135,6 +137,7 @@ const props = withDefaults(defineProps<{
mention?: Misskey.entities.User;
specified?: Misskey.entities.User;
initialText?: string;
+ initialCw?: string;
initialVisibility?: (typeof Misskey.noteVisibilities)[number];
initialFiles?: Misskey.entities.DriveFile[];
initialLocalOnly?: boolean;
@@ -144,7 +147,7 @@ const props = withDefaults(defineProps<{
fixed?: boolean;
autofocus?: boolean;
freezeAfterPosted?: boolean;
- editId?: Misskey.entities.Note["id"];
+ editId?: Misskey.entities.Note['id'];
mock?: boolean;
}>(), {
initialVisibleUsers: () => [],
@@ -163,41 +166,42 @@ const emit = defineEmits<{
(ev: 'fileChangeSensitive', fileId: string, to: boolean): void;
}>();
-const textareaEl = $shallowRef<HTMLTextAreaElement | null>(null);
-const cwInputEl = $shallowRef<HTMLInputElement | null>(null);
-const hashtagsInputEl = $shallowRef<HTMLInputElement | null>(null);
-const visibilityButton = $shallowRef<HTMLElement | null>(null);
+const textareaEl = shallowRef<HTMLTextAreaElement | null>(null);
+const cwInputEl = shallowRef<HTMLInputElement | null>(null);
+const hashtagsInputEl = shallowRef<HTMLInputElement | null>(null);
+const visibilityButton = shallowRef<HTMLElement | null>(null);
-let posting = $ref(false);
-let posted = $ref(false);
-let text = $ref(props.initialText ?? '');
-let files = $ref(props.initialFiles ?? []);
-let poll = $ref<{
+const posting = ref(false);
+const posted = ref(false);
+const text = ref(props.initialText ?? '');
+const files = ref(props.initialFiles ?? []);
+const poll = ref<{
choices: string[];
multiple: boolean;
expiresAt: string | null;
expiredAfter: string | null;
} | null>(null);
-let useCw = $ref(false);
-let showPreview = $ref(defaultStore.state.showPreview);
-watch($$(showPreview), () => defaultStore.set('showPreview', showPreview));
-let cw = $ref<string | null>(null);
-let localOnly = $ref<boolean>(props.initialLocalOnly ?? defaultStore.state.rememberNoteVisibility ? defaultStore.state.localOnly : defaultStore.state.defaultNoteLocalOnly);
-let visibility = $ref(props.initialVisibility ?? (defaultStore.state.rememberNoteVisibility ? defaultStore.state.visibility : defaultStore.state.defaultNoteVisibility) as typeof Misskey.noteVisibilities[number]);
-let visibleUsers = $ref([]);
+const useCw = ref<boolean>(!!props.initialCw);
+const showPreview = ref(defaultStore.state.showPreview);
+watch(showPreview, () => defaultStore.set('showPreview', showPreview.value));
+const cw = ref<string | null>(props.initialCw ?? null);
+const localOnly = ref<boolean>(props.initialLocalOnly ?? defaultStore.state.rememberNoteVisibility ? defaultStore.state.localOnly : defaultStore.state.defaultNoteLocalOnly);
+const visibility = ref(props.initialVisibility ?? (defaultStore.state.rememberNoteVisibility ? defaultStore.state.visibility : defaultStore.state.defaultNoteVisibility) as typeof Misskey.noteVisibilities[number]);
+const visibleUsers = ref([]);
if (props.initialVisibleUsers) {
props.initialVisibleUsers.forEach(pushVisibleUser);
}
-let reactionAcceptance = $ref(defaultStore.state.reactionAcceptance);
-let autocomplete = $ref(null);
-let draghover = $ref(false);
-let quoteId = $ref(null);
-let hasNotSpecifiedMentions = $ref(false);
-let recentHashtags = $ref(JSON.parse(miLocalStorage.getItem('hashtags') ?? '[]'));
-let imeText = $ref('');
-let showingOptions = $ref(false);
+const reactionAcceptance = ref(defaultStore.state.reactionAcceptance);
+const autocomplete = ref(null);
+const draghover = ref(false);
+const quoteId = ref(null);
+const hasNotSpecifiedMentions = ref(false);
+const recentHashtags = ref(JSON.parse(miLocalStorage.getItem('hashtags') ?? '[]'));
+const imeText = ref('');
+const showingOptions = ref(false);
+const textAreaReadOnly = ref(false);
-const draftKey = $computed((): string => {
+const draftKey = computed((): string => {
let key = props.channel ? `channel:${props.channel.id}` : '';
if (props.renote) {
@@ -211,7 +215,7 @@ const draftKey = $computed((): string => {
return key;
});
-const placeholder = $computed((): string => {
+const placeholder = computed((): string => {
if (props.renote) {
return i18n.ts._postForm.quotePlaceholder;
} else if (props.reply) {
@@ -231,7 +235,7 @@ const placeholder = $computed((): string => {
}
});
-const submitText = $computed((): string => {
+const submitText = computed((): string => {
return props.renote
? i18n.ts.quote
: props.reply
@@ -239,45 +243,45 @@ const submitText = $computed((): string => {
: i18n.ts.note;
});
-const textLength = $computed((): number => {
- return (text + imeText).trim().length;
+const textLength = computed((): number => {
+ return (text.value + imeText.value).trim().length;
});
-const maxTextLength = $computed((): number => {
+const maxTextLength = computed((): number => {
return instance ? instance.maxNoteTextLength : 1000;
});
-const canPost = $computed((): boolean => {
- return !props.mock && !posting && !posted &&
- (1 <= textLength || 1 <= files.length || !!poll || !!props.renote) &&
- (textLength <= maxTextLength) &&
- (!poll || poll.choices.length >= 2);
+const canPost = computed((): boolean => {
+ return !props.mock && !posting.value && !posted.value &&
+ (1 <= textLength.value || 1 <= files.value.length || !!poll.value || !!props.renote) &&
+ (textLength.value <= maxTextLength.value) &&
+ (!poll.value || poll.value.choices.length >= 2);
});
-const withHashtags = $computed(defaultStore.makeGetterSetter('postFormWithHashtags'));
-const hashtags = $computed(defaultStore.makeGetterSetter('postFormHashtags'));
+const withHashtags = computed(defaultStore.makeGetterSetter('postFormWithHashtags'));
+const hashtags = computed(defaultStore.makeGetterSetter('postFormHashtags'));
-watch($$(text), () => {
+watch(text, () => {
checkMissingMention();
}, { immediate: true });
-watch($$(visibility), () => {
+watch(visibility, () => {
checkMissingMention();
}, { immediate: true });
-watch($$(visibleUsers), () => {
+watch(visibleUsers, () => {
checkMissingMention();
}, {
deep: true,
});
if (props.mention) {
- text = props.mention.host ? `@${props.mention.username}@${toASCII(props.mention.host)}` : `@${props.mention.username}`;
- text += ' ';
+ text.value = props.mention.host ? `@${props.mention.username}@${toASCII(props.mention.host)}` : `@${props.mention.username}`;
+ text.value += ' ';
}
if (props.reply && (props.reply.user.username !== $i.username || (props.reply.user.host != null && props.reply.user.host !== host))) {
- text = `@${props.reply.user.username}${props.reply.user.host != null ? '@' + toASCII(props.reply.user.host) : ''} `;
+ text.value = `@${props.reply.user.username}${props.reply.user.host != null ? '@' + toASCII(props.reply.user.host) : ''} `;
}
if (props.reply && props.reply.text != null) {
@@ -295,32 +299,32 @@ if (props.reply && props.reply.text != null) {
if ($i.username === x.username && (x.host == null || x.host === host)) continue;
// 重複は除外
- if (text.includes(`${mention} `)) continue;
+ if (text.value.includes(`${mention} `)) continue;
- text += `${mention} `;
+ text.value += `${mention} `;
}
}
-if ($i?.isSilenced && visibility === 'public') {
- visibility = 'home';
+if ($i?.isSilenced && visibility.value === 'public') {
+ visibility.value = 'home';
}
if (props.channel) {
- visibility = 'public';
- localOnly = true; // TODO: チャンネルが連合するようになった折には消す
+ visibility.value = 'public';
+ localOnly.value = true; // TODO: チャンネルが連合するようになった折には消す
}
// 公開以外へのリプライ時は元の公開範囲を引き継ぐ
if (props.reply && ['home', 'followers', 'specified'].includes(props.reply.visibility)) {
- if (props.reply.visibility === 'home' && visibility === 'followers') {
- visibility = 'followers';
- } else if (['home', 'followers'].includes(props.reply.visibility) && visibility === 'specified') {
- visibility = 'specified';
+ if (props.reply.visibility === 'home' && visibility.value === 'followers') {
+ visibility.value = 'followers';
+ } else if (['home', 'followers'].includes(props.reply.visibility) && visibility.value === 'specified') {
+ visibility.value = 'specified';
} else {
- visibility = props.reply.visibility;
+ visibility.value = props.reply.visibility;
}
- if (visibility === 'specified') {
+ if (visibility.value === 'specified') {
if (props.reply.visibleUserIds) {
os.api('users/show', {
userIds: props.reply.visibleUserIds.filter(uid => uid !== $i.id && uid !== props.reply.userId),
@@ -338,24 +342,24 @@ if (props.reply && ['home', 'followers', 'specified'].includes(props.reply.visib
}
if (props.specified) {
- visibility = 'specified';
+ visibility.value = 'specified';
pushVisibleUser(props.specified);
}
// keep cw when reply
if (defaultStore.state.keepCw && props.reply && props.reply.cw) {
- useCw = true;
- cw = props.reply.cw;
+ useCw.value = true;
+ cw.value = props.reply.cw;
}
function watchForDraft() {
- watch($$(text), () => saveDraft());
- watch($$(useCw), () => saveDraft());
- watch($$(cw), () => saveDraft());
- watch($$(poll), () => saveDraft());
- watch($$(files), () => saveDraft(), { deep: true });
- watch($$(visibility), () => saveDraft());
- watch($$(localOnly), () => saveDraft());
+ watch(text, () => saveDraft());
+ watch(useCw, () => saveDraft());
+ watch(cw, () => saveDraft());
+ watch(poll, () => saveDraft());
+ watch(files, () => saveDraft(), { deep: true });
+ watch(visibility, () => saveDraft());
+ watch(localOnly, () => saveDraft());
}
function MFMWindow() {
@@ -363,36 +367,36 @@ function MFMWindow() {
}
function checkMissingMention() {
- if (visibility === 'specified') {
- const ast = mfm.parse(text);
+ if (visibility.value === 'specified') {
+ const ast = mfm.parse(text.value);
for (const x of extractMentions(ast)) {
- if (!visibleUsers.some(u => (u.username === x.username) && (u.host === x.host))) {
- hasNotSpecifiedMentions = true;
+ if (!visibleUsers.value.some(u => (u.username === x.username) && (u.host === x.host))) {
+ hasNotSpecifiedMentions.value = true;
return;
}
}
- hasNotSpecifiedMentions = false;
}
+ hasNotSpecifiedMentions.value = false;
}
function addMissingMention() {
- const ast = mfm.parse(text);
+ const ast = mfm.parse(text.value);
for (const x of extractMentions(ast)) {
- if (!visibleUsers.some(u => (u.username === x.username) && (u.host === x.host))) {
+ if (!visibleUsers.value.some(u => (u.username === x.username) && (u.host === x.host))) {
os.api('users/show', { username: x.username, host: x.host }).then(user => {
- visibleUsers.push(user);
+ visibleUsers.value.push(user);
});
}
}
}
function togglePoll() {
- if (poll) {
- poll = null;
+ if (poll.value) {
+ poll.value = null;
} else {
- poll = {
+ poll.value = {
choices: ['', ''],
multiple: false,
expiresAt: null,
@@ -402,13 +406,13 @@ function togglePoll() {
}
function addTag(tag: string) {
- insertTextAtCursor(textareaEl, ` #${tag} `);
+ insertTextAtCursor(textareaEl.value, ` #${tag} `);
}
function focus() {
- if (textareaEl) {
- textareaEl.focus();
- textareaEl.setSelectionRange(textareaEl.value.length, textareaEl.value.length);
+ if (textareaEl.value) {
+ textareaEl.value.focus();
+ textareaEl.value.setSelectionRange(textareaEl.value.value.length, textareaEl.value.value.length);
}
}
@@ -417,55 +421,55 @@ function chooseFileFrom(ev) {
selectFiles(ev.currentTarget ?? ev.target, i18n.ts.attachFile).then(files_ => {
for (const file of files_) {
- files.push(file);
+ files.value.push(file);
}
});
}
function detachFile(id) {
- files = files.filter(x => x.id !== id);
+ files.value = files.value.filter(x => x.id !== id);
}
function updateFileSensitive(file, sensitive) {
if (props.mock) {
emit('fileChangeSensitive', file.id, sensitive);
}
- files[files.findIndex(x => x.id === file.id)].isSensitive = sensitive;
+ files.value[files.value.findIndex(x => x.id === file.id)].isSensitive = sensitive;
}
function updateFileName(file, name) {
- files[files.findIndex(x => x.id === file.id)].name = name;
+ files.value[files.value.findIndex(x => x.id === file.id)].name = name;
}
function replaceFile(file: Misskey.entities.DriveFile, newFile: Misskey.entities.DriveFile): void {
- files[files.findIndex(x => x.id === file.id)] = newFile;
+ files.value[files.value.findIndex(x => x.id === file.id)] = newFile;
}
function upload(file: File, name?: string): void {
if (props.mock) return;
uploadFile(file, defaultStore.state.uploadFolder, name).then(res => {
- files.push(res);
+ files.value.push(res);
});
}
function setVisibility() {
if (props.channel) {
- visibility = 'public';
- localOnly = true; // TODO: チャンネルが連合するようになった折には消す
+ visibility.value = 'public';
+ localOnly.value = true; // TODO: チャンネルが連合するようになった折には消す
return;
}
os.popup(defineAsyncComponent(() => import('@/components/MkVisibilityPicker.vue')), {
- currentVisibility: visibility,
+ currentVisibility: visibility.value,
isSilenced: $i?.isSilenced,
- localOnly: localOnly,
- src: visibilityButton,
+ localOnly: localOnly.value,
+ src: visibilityButton.value,
}, {
changeVisibility: v => {
- visibility = v;
+ visibility.value = v;
if (defaultStore.state.rememberNoteVisibility) {
- defaultStore.set('visibility', visibility);
+ defaultStore.set('visibility', visibility.value);
}
},
}, 'closed');
@@ -473,14 +477,14 @@ function setVisibility() {
async function toggleLocalOnly() {
if (props.channel) {
- visibility = 'public';
- localOnly = true; // TODO: チャンネルが連合するようになった折には消す
+ visibility.value = 'public';
+ localOnly.value = true; // TODO: チャンネルが連合するようになった折には消す
return;
}
const neverShowInfo = miLocalStorage.getItem('neverShowLocalOnlyInfo');
- if (!localOnly && neverShowInfo !== 'true') {
+ if (!localOnly.value && neverShowInfo !== 'true') {
const confirm = await os.actions({
type: 'question',
title: i18n.ts.disableFederationConfirm,
@@ -510,7 +514,7 @@ async function toggleLocalOnly() {
}
}
- localOnly = !localOnly;
+ localOnly.value = !localOnly.value;
}
async function toggleReactionAcceptance() {
@@ -523,15 +527,15 @@ async function toggleReactionAcceptance() {
{ value: 'nonSensitiveOnlyForLocalLikeOnlyForRemote' as const, text: i18n.ts.nonSensitiveOnlyForLocalLikeOnlyForRemote },
{ value: 'likeOnly' as const, text: i18n.ts.likeOnly },
],
- default: reactionAcceptance,
+ default: reactionAcceptance.value,
});
if (select.canceled) return;
- reactionAcceptance = select.result;
+ reactionAcceptance.value = select.result;
}
function pushVisibleUser(user) {
- if (!visibleUsers.some(u => u.username === user.username && u.host === user.host)) {
- visibleUsers.push(user);
+ if (!visibleUsers.value.some(u => u.username === user.username && u.host === user.host)) {
+ visibleUsers.value.push(user);
}
}
@@ -539,34 +543,34 @@ function addVisibleUser() {
os.selectUser().then(user => {
pushVisibleUser(user);
- if (!text.toLowerCase().includes(`@${user.username.toLowerCase()}`)) {
- text = `@${Misskey.acct.toString(user)} ${text}`;
+ if (!text.value.toLowerCase().includes(`@${user.username.toLowerCase()}`)) {
+ text.value = `@${Misskey.acct.toString(user)} ${text.value}`;
}
});
}
function removeVisibleUser(user) {
- visibleUsers = erase(user, visibleUsers);
+ visibleUsers.value = erase(user, visibleUsers.value);
}
function clear() {
- text = '';
- files = [];
- poll = null;
- quoteId = null;
+ text.value = '';
+ files.value = [];
+ poll.value = null;
+ quoteId.value = null;
}
function onKeydown(ev: KeyboardEvent) {
- if (ev.key === 'Enter' && (ev.ctrlKey || ev.metaKey) && canPost) post();
+ if (ev.key === 'Enter' && (ev.ctrlKey || ev.metaKey) && canPost.value) post();
if (ev.key === 'Escape') emit('esc');
}
function onCompositionUpdate(ev: CompositionEvent) {
- imeText = ev.data;
+ imeText.value = ev.data;
}
function onCompositionEnd(ev: CompositionEvent) {
- imeText = '';
+ imeText.value = '';
}
async function onPaste(ev: ClipboardEvent) {
@@ -584,7 +588,7 @@ async function onPaste(ev: ClipboardEvent) {
const paste = ev.clipboardData.getData('text');
- if (!props.renote && !quoteId && paste.startsWith(url + '/notes/')) {
+ if (!props.renote && !quoteId.value && paste.startsWith(url + '/notes/')) {
ev.preventDefault();
os.confirm({
@@ -592,11 +596,11 @@ async function onPaste(ev: ClipboardEvent) {
text: i18n.ts.quoteQuestion,
}).then(({ canceled }) => {
if (canceled) {
- insertTextAtCursor(textareaEl, paste);
+ insertTextAtCursor(textareaEl.value, paste);
return;
}
- quoteId = paste.substring(url.length).match(/^\/notes\/(.+?)\/?$/)[1];
+ quoteId.value = paste.substring(url.length).match(/^\/notes\/(.+?)\/?$/)[1];
});
}
}
@@ -607,7 +611,7 @@ function onDragover(ev) {
const isDriveFile = ev.dataTransfer.types[0] === _DATA_TRANSFER_DRIVE_FILE_;
if (isFile || isDriveFile) {
ev.preventDefault();
- draghover = true;
+ draghover.value = true;
switch (ev.dataTransfer.effectAllowed) {
case 'all':
case 'uninitialized':
@@ -628,15 +632,15 @@ function onDragover(ev) {
}
function onDragenter(ev) {
- draghover = true;
+ draghover.value = true;
}
function onDragleave(ev) {
- draghover = false;
+ draghover.value = false;
}
function onDrop(ev): void {
- draghover = false;
+ draghover.value = false;
// ファイルだったら
if (ev.dataTransfer.files.length > 0) {
@@ -649,7 +653,7 @@ function onDrop(ev): void {
const driveFile = ev.dataTransfer.getData(_DATA_TRANSFER_DRIVE_FILE_);
if (driveFile != null && driveFile !== '') {
const file = JSON.parse(driveFile);
- files.push(file);
+ files.value.push(file);
ev.preventDefault();
}
//#endregion
@@ -660,16 +664,16 @@ function saveDraft() {
const draftData = JSON.parse(miLocalStorage.getItem('drafts') ?? '{}');
- draftData[draftKey] = {
+ draftData[draftKey.value] = {
updatedAt: new Date(),
data: {
- text: text,
- useCw: useCw,
- cw: cw,
- visibility: visibility,
- localOnly: localOnly,
- files: files,
- poll: poll,
+ text: text.value,
+ useCw: useCw.value,
+ cw: cw.value,
+ visibility: visibility.value,
+ localOnly: localOnly.value,
+ files: files.value,
+ poll: poll.value,
},
};
@@ -679,13 +683,13 @@ function saveDraft() {
function deleteDraft() {
const draftData = JSON.parse(miLocalStorage.getItem('drafts') ?? '{}');
- delete draftData[draftKey];
+ delete draftData[draftKey.value];
miLocalStorage.setItem('drafts', JSON.stringify(draftData));
}
async function post(ev?: MouseEvent) {
- if (useCw && (cw == null || cw.trim() === '')) {
+ if (useCw.value && (cw.value == null || cw.value.trim() === '')) {
os.alert({
type: 'error',
text: i18n.ts.cwNotationRequired,
@@ -704,13 +708,13 @@ async function post(ev?: MouseEvent) {
if (props.mock) return;
const annoying =
- text.includes('$[x2') ||
- text.includes('$[x3') ||
- text.includes('$[x4') ||
- text.includes('$[scale') ||
- text.includes('$[position');
+ text.value.includes('$[x2') ||
+ text.value.includes('$[x3') ||
+ text.value.includes('$[x4') ||
+ text.value.includes('$[scale') ||
+ text.value.includes('$[position');
- if (annoying && visibility === 'public') {
+ if (annoying && visibility.value === 'public') {
const { canceled, result } = await os.actions({
type: 'warning',
text: i18n.ts.thisPostMayBeAnnoying,
@@ -730,27 +734,27 @@ async function post(ev?: MouseEvent) {
if (canceled) return;
if (result === 'cancel') return;
if (result === 'home') {
- visibility = 'home';
+ visibility.value = 'home';
}
}
let postData = {
- text: text === '' ? null : text,
- fileIds: files.length > 0 ? files.map(f => f.id) : undefined,
+ text: text.value === '' ? null : text.value,
+ fileIds: files.value.length > 0 ? files.value.map(f => f.id) : undefined,
replyId: props.reply ? props.reply.id : undefined,
- renoteId: props.renote ? props.renote.id : quoteId ? quoteId : undefined,
+ renoteId: props.renote ? props.renote.id : quoteId.value ? quoteId.value : undefined,
channelId: props.channel ? props.channel.id : undefined,
- poll: poll,
- cw: useCw ? cw ?? '' : null,
- localOnly: localOnly,
- visibility: visibility,
- visibleUserIds: visibility === 'specified' ? visibleUsers.map(u => u.id) : undefined,
- reactionAcceptance,
+ poll: poll.value,
+ cw: useCw.value ? cw.value ?? '' : null,
+ localOnly: localOnly.value,
+ visibility: visibility.value,
+ visibleUserIds: visibility.value === 'specified' ? visibleUsers.value.map(u => u.id) : undefined,
+ reactionAcceptance: reactionAcceptance.value,
editId: props.editId ? props.editId : undefined,
};
- if (withHashtags && hashtags && hashtags.trim() !== '') {
- const hashtags_ = hashtags.trim().split(' ').map(x => x.startsWith('#') ? x : '#' + x).join(' ');
+ if (withHashtags.value && hashtags.value && hashtags.value.trim() !== '') {
+ const hashtags_ = hashtags.value.trim().split(' ').map(x => x.startsWith('#') ? x : '#' + x).join(' ');
postData.text = postData.text ? `${postData.text} ${hashtags_}` : hashtags_;
}
@@ -767,15 +771,15 @@ async function post(ev?: MouseEvent) {
let token = undefined;
- if (postAccount) {
+ if (postAccount.value) {
const storedAccounts = await getAccounts();
- token = storedAccounts.find(x => x.id === postAccount.id)?.token;
+ token = storedAccounts.find(x => x.id === postAccount.value.id)?.token;
}
- posting = true;
- os.api(postData.editId ? "notes/edit" : "notes/create", postData, token).then(() => {
+ posting.value = true;
+ os.api(postData.editId ? 'notes/edit' : 'notes/create', postData, token).then(() => {
if (props.freezeAfterPosted) {
- posted = true;
+ posted.value = true;
} else {
clear();
}
@@ -787,8 +791,8 @@ async function post(ev?: MouseEvent) {
const history = JSON.parse(miLocalStorage.getItem('hashtags') ?? '[]') as string[];
miLocalStorage.setItem('hashtags', JSON.stringify(unique(hashtags_.concat(history))));
}
- posting = false;
- postAccount = null;
+ posting.value = false;
+ postAccount.value = null;
incNotesCount();
if (notesCount === 1) {
@@ -833,7 +837,7 @@ async function post(ev?: MouseEvent) {
}
});
}).catch(err => {
- posting = false;
+ posting.value = false;
os.alert({
type: 'error',
text: err.message + '\n' + (err as any).id,
@@ -847,12 +851,23 @@ function cancel() {
function insertMention() {
os.selectUser().then(user => {
- insertTextAtCursor(textareaEl, '@' + Misskey.acct.toString(user) + ' ');
+ insertTextAtCursor(textareaEl.value, '@' + Misskey.acct.toString(user) + ' ');
});
}
async function insertEmoji(ev: MouseEvent) {
- os.openEmojiPicker(ev.currentTarget ?? ev.target, {}, textareaEl);
+ textAreaReadOnly.value = true;
+
+ emojiPicker.show(
+ ev.currentTarget ?? ev.target,
+ emoji => {
+ insertTextAtCursor(textareaEl.value, emoji);
+ },
+ () => {
+ textAreaReadOnly.value = false;
+ nextTick(() => focus());
+ },
+ );
}
function showActions(ev) {
@@ -860,17 +875,17 @@ function showActions(ev) {
text: action.title,
action: () => {
action.handler({
- text: text,
- cw: cw,
+ text: text.value,
+ cw: cw.value,
}, (key, value) => {
- if (key === 'text') { text = value; }
- if (key === 'cw') { useCw = value !== null; cw = value; }
+ if (key === 'text') { text.value = value; }
+ if (key === 'cw') { useCw.value = value !== null; cw.value = value; }
});
},
})), ev.currentTarget ?? ev.target);
}
-let postAccount = $ref<Misskey.entities.UserDetailed | null>(null);
+const postAccount = ref<Misskey.entities.UserDetailed | null>(null);
function openAccountMenu(ev: MouseEvent) {
if (props.mock) return;
@@ -878,12 +893,12 @@ function openAccountMenu(ev: MouseEvent) {
openAccountMenu_({
withExtraOperation: false,
includeCurrentAccount: true,
- active: postAccount != null ? postAccount.id : $i.id,
+ active: postAccount.value != null ? postAccount.value.id : $i.id,
onChoose: (account) => {
if (account.id === $i.id) {
- postAccount = null;
+ postAccount.value = null;
} else {
- postAccount = account;
+ postAccount.value = account;
}
},
}, ev);
@@ -899,23 +914,23 @@ onMounted(() => {
}
// TODO: detach when unmount
- new Autocomplete(textareaEl, $$(text));
- new Autocomplete(cwInputEl, $$(cw));
- new Autocomplete(hashtagsInputEl, $$(hashtags));
+ new Autocomplete(textareaEl.value, text);
+ new Autocomplete(cwInputEl.value, cw);
+ new Autocomplete(hashtagsInputEl.value, hashtags);
nextTick(() => {
// 書きかけの投稿を復元
if (!props.instant && !props.mention && !props.specified && !props.mock) {
- const draft = JSON.parse(miLocalStorage.getItem('drafts') ?? '{}')[draftKey];
+ const draft = JSON.parse(miLocalStorage.getItem('drafts') ?? '{}')[draftKey.value];
if (draft) {
- text = draft.data.text;
- useCw = draft.data.useCw;
- cw = draft.data.cw;
- visibility = draft.data.visibility;
- localOnly = draft.data.localOnly;
- files = (draft.data.files || []).filter(draftFile => draftFile);
+ text.value = draft.data.text;
+ useCw.value = draft.data.useCw;
+ cw.value = draft.data.cw;
+ visibility.value = draft.data.visibility;
+ localOnly.value = draft.data.localOnly;
+ files.value = (draft.data.files || []).filter(draftFile => draftFile);
if (draft.data.poll) {
- poll = draft.data.poll;
+ poll.value = draft.data.poll;
}
}
}
@@ -923,21 +938,21 @@ onMounted(() => {
// 削除して編集
if (props.initialNote) {
const init = props.initialNote;
- text = init.text ? init.text : '';
- files = init.files;
- cw = init.cw;
- useCw = init.cw != null;
+ text.value = init.text ? init.text : '';
+ files.value = init.files;
+ cw.value = init.cw;
+ useCw.value = init.cw != null;
if (init.poll) {
- poll = {
+ poll.value = {
choices: init.poll.choices.map(x => x.text),
multiple: init.poll.multiple,
expiresAt: init.poll.expiresAt ? new Date(init.poll.expiresAt).getTime().toString() : null,
expiredAfter: init.poll.expiredAfter ? new Date(init.poll.expiredAfter).getTime().toString() : null,
};
}
- visibility = init.visibility;
- localOnly = init.localOnly;
- quoteId = init.renote ? init.renote.id : null;
+ visibility.value = init.visibility;
+ localOnly.value = init.localOnly;
+ quoteId.value = init.renote ? init.renote.id : null;
}
nextTick(() => watchForDraft());
@@ -1031,6 +1046,16 @@ defineExpose({
}
}
+.colorBar {
+ position: absolute;
+ top: 0px;
+ left: 12px;
+ width: 5px;
+ height: 100% ;
+ border-radius: 999px;
+ pointer-events: none;
+}
+
.submitInner {
padding: 0 12px;
line-height: 34px;
@@ -1066,8 +1091,9 @@ defineExpose({
.visibility {
overflow: clip;
- text-overflow: ellipsis;
- white-space: nowrap;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ max-width: 210px;
&:enabled {
> .headerRightButtonText {
@@ -1288,5 +1314,6 @@ defineExpose({
.headerRight {
gap: 0;
}
+
}
</style>
diff --git a/packages/frontend/src/components/MkPostFormDialog.vue b/packages/frontend/src/components/MkPostFormDialog.vue
index 25a8788a38..cd25077bfb 100644
--- a/packages/frontend/src/components/MkPostFormDialog.vue
+++ b/packages/frontend/src/components/MkPostFormDialog.vue
@@ -10,7 +10,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { } from 'vue';
+import { shallowRef } from 'vue';
import * as Misskey from 'misskey-js';
import MkModal from '@/components/MkModal.vue';
import MkPostForm from '@/components/MkPostForm.vue';
@@ -22,6 +22,7 @@ const props = defineProps<{
mention?: Misskey.entities.User;
specified?: Misskey.entities.User;
initialText?: string;
+ initialCw?: string;
initialVisibility?: typeof Misskey.noteVisibilities;
initialFiles?: Misskey.entities.DriveFile[];
initialLocalOnly?: boolean;
@@ -37,11 +38,11 @@ const emit = defineEmits<{
(ev: 'closed'): void;
}>();
-let modal = $shallowRef<InstanceType<typeof MkModal>>();
-let form = $shallowRef<InstanceType<typeof MkPostForm>>();
+const modal = shallowRef<InstanceType<typeof MkModal>>();
+const form = shallowRef<InstanceType<typeof MkPostForm>>();
function onPosted() {
- modal.close({
+ modal.value.close({
useSendAnimation: true,
});
}
diff --git a/packages/frontend/src/components/MkPullToRefresh.vue b/packages/frontend/src/components/MkPullToRefresh.vue
index 9f50f7ad5d..e963697997 100644
--- a/packages/frontend/src/components/MkPullToRefresh.vue
+++ b/packages/frontend/src/components/MkPullToRefresh.vue
@@ -23,8 +23,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { onMounted, onUnmounted, watch } from 'vue';
-import { deviceKind } from '@/scripts/device-kind.js';
+import { onMounted, onUnmounted, ref, shallowRef } from 'vue';
import { i18n } from '@/i18n.js';
import { getScrollContainer } from '@/scripts/scroll.js';
@@ -35,15 +34,15 @@ const RELEASE_TRANSITION_DURATION = 200;
const PULL_BRAKE_BASE = 1.5;
const PULL_BRAKE_FACTOR = 170;
-let isPullStart = $ref(false);
-let isPullEnd = $ref(false);
-let isRefreshing = $ref(false);
-let pullDistance = $ref(0);
+const isPullStart = ref(false);
+const isPullEnd = ref(false);
+const isRefreshing = ref(false);
+const pullDistance = ref(0);
let supportPointerDesktop = false;
let startScreenY: number | null = null;
-const rootEl = $shallowRef<HTMLDivElement>();
+const rootEl = shallowRef<HTMLDivElement>();
let scrollEl: HTMLElement | null = null;
let disabled = false;
@@ -66,17 +65,17 @@ function getScreenY(event) {
}
function moveStart(event) {
- if (!isPullStart && !isRefreshing && !disabled) {
- isPullStart = true;
+ if (!isPullStart.value && !isRefreshing.value && !disabled) {
+ isPullStart.value = true;
startScreenY = getScreenY(event);
- pullDistance = 0;
+ pullDistance.value = 0;
}
}
function moveBySystem(to: number): Promise<void> {
return new Promise(r => {
- const startHeight = pullDistance;
- const overHeight = pullDistance - to;
+ const startHeight = pullDistance.value;
+ const overHeight = pullDistance.value - to;
if (overHeight < 1) {
r();
return;
@@ -85,36 +84,36 @@ function moveBySystem(to: number): Promise<void> {
let intervalId = setInterval(() => {
const time = Date.now() - startTime;
if (time > RELEASE_TRANSITION_DURATION) {
- pullDistance = to;
+ pullDistance.value = to;
clearInterval(intervalId);
r();
return;
}
const nextHeight = startHeight - (overHeight / RELEASE_TRANSITION_DURATION) * time;
- if (pullDistance < nextHeight) return;
- pullDistance = nextHeight;
+ if (pullDistance.value < nextHeight) return;
+ pullDistance.value = nextHeight;
}, 1);
});
}
async function fixOverContent() {
- if (pullDistance > FIRE_THRESHOLD) {
+ if (pullDistance.value > FIRE_THRESHOLD) {
await moveBySystem(FIRE_THRESHOLD);
}
}
async function closeContent() {
- if (pullDistance > 0) {
+ if (pullDistance.value > 0) {
await moveBySystem(0);
}
}
function moveEnd() {
- if (isPullStart && !isRefreshing) {
+ if (isPullStart.value && !isRefreshing.value) {
startScreenY = null;
- if (isPullEnd) {
- isPullEnd = false;
- isRefreshing = true;
+ if (isPullEnd.value) {
+ isPullEnd.value = false;
+ isRefreshing.value = true;
fixOverContent().then(() => {
emit('refresh');
props.refresher().then(() => {
@@ -122,17 +121,17 @@ function moveEnd() {
});
});
} else {
- closeContent().then(() => isPullStart = false);
+ closeContent().then(() => isPullStart.value = false);
}
}
}
function moving(event: TouchEvent | PointerEvent) {
- if (!isPullStart || isRefreshing || disabled) return;
+ if (!isPullStart.value || isRefreshing.value || disabled) return;
- if ((scrollEl?.scrollTop ?? 0) > (supportPointerDesktop ? SCROLL_STOP : SCROLL_STOP + pullDistance)) {
- pullDistance = 0;
- isPullEnd = false;
+ if ((scrollEl?.scrollTop ?? 0) > (supportPointerDesktop ? SCROLL_STOP : SCROLL_STOP + pullDistance.value)) {
+ pullDistance.value = 0;
+ isPullEnd.value = false;
moveEnd();
return;
}
@@ -143,13 +142,13 @@ function moving(event: TouchEvent | PointerEvent) {
const moveScreenY = getScreenY(event);
const moveHeight = moveScreenY - startScreenY!;
- pullDistance = Math.min(Math.max(moveHeight, 0), MAX_PULL_DISTANCE);
+ pullDistance.value = Math.min(Math.max(moveHeight, 0), MAX_PULL_DISTANCE);
- if (pullDistance > 0) {
+ if (pullDistance.value > 0) {
if (event.cancelable) event.preventDefault();
}
- isPullEnd = pullDistance >= FIRE_THRESHOLD;
+ isPullEnd.value = pullDistance.value >= FIRE_THRESHOLD;
}
/**
@@ -159,8 +158,8 @@ function moving(event: TouchEvent | PointerEvent) {
*/
function refreshFinished() {
closeContent().then(() => {
- isPullStart = false;
- isRefreshing = false;
+ isPullStart.value = false;
+ isRefreshing.value = false;
});
}
@@ -182,26 +181,26 @@ function onScrollContainerScroll() {
}
function registerEventListenersForReadyToPull() {
- if (rootEl == null) return;
- rootEl.addEventListener('touchstart', moveStart, { passive: true });
- rootEl.addEventListener('touchmove', moving, { passive: false }); // passive: falseにしないとpreventDefaultが使えない
+ if (rootEl.value == null) return;
+ rootEl.value.addEventListener('touchstart', moveStart, { passive: true });
+ rootEl.value.addEventListener('touchmove', moving, { passive: false }); // passive: falseにしないとpreventDefaultが使えない
}
function unregisterEventListenersForReadyToPull() {
- if (rootEl == null) return;
- rootEl.removeEventListener('touchstart', moveStart);
- rootEl.removeEventListener('touchmove', moving);
+ if (rootEl.value == null) return;
+ rootEl.value.removeEventListener('touchstart', moveStart);
+ rootEl.value.removeEventListener('touchmove', moving);
}
onMounted(() => {
- if (rootEl == null) return;
+ if (rootEl.value == null) return;
- scrollEl = getScrollContainer(rootEl);
+ scrollEl = getScrollContainer(rootEl.value);
if (scrollEl == null) return;
scrollEl.addEventListener('scroll', onScrollContainerScroll, { passive: true });
- rootEl.addEventListener('touchend', moveEnd, { passive: true });
+ rootEl.value.addEventListener('touchend', moveEnd, { passive: true });
registerEventListenersForReadyToPull();
});
diff --git a/packages/frontend/src/components/MkPushNotificationAllowButton.vue b/packages/frontend/src/components/MkPushNotificationAllowButton.vue
index ba64775298..ebbd5e6cdc 100644
--- a/packages/frontend/src/components/MkPushNotificationAllowButton.vue
+++ b/packages/frontend/src/components/MkPushNotificationAllowButton.vue
@@ -41,6 +41,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script setup lang="ts">
+import { ref } from 'vue';
import { $i, getAccounts } from '@/account.js';
import MkButton from '@/components/MkButton.vue';
import { instance } from '@/instance.js';
@@ -62,26 +63,26 @@ defineProps<{
}>();
// ServiceWorker registration
-let registration = $ref<ServiceWorkerRegistration | undefined>();
+const registration = ref<ServiceWorkerRegistration | undefined>();
// If this browser supports push notification
-let supported = $ref(false);
+const supported = ref(false);
// If this browser has already subscribed to push notification
-let pushSubscription = $ref<PushSubscription | null>(null);
-let pushRegistrationInServer = $ref<{ state?: string; key?: string; userId: string; endpoint: string; sendReadMessage: boolean; } | undefined>();
+const pushSubscription = ref<PushSubscription | null>(null);
+const pushRegistrationInServer = ref<{ state?: string; key?: string; userId: string; endpoint: string; sendReadMessage: boolean; } | undefined>();
function subscribe() {
- if (!registration || !supported || !instance.swPublickey) return;
+ if (!registration.value || !supported.value || !instance.swPublickey) return;
// SEE: https://developer.mozilla.org/en-US/docs/Web/API/PushManager/subscribe#Parameters
- return promiseDialog(registration.pushManager.subscribe({
+ return promiseDialog(registration.value.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: urlBase64ToUint8Array(instance.swPublickey),
})
.then(async subscription => {
- pushSubscription = subscription;
+ pushSubscription.value = subscription;
// Register
- pushRegistrationInServer = await api('sw/register', {
+ pushRegistrationInServer.value = await api('sw/register', {
endpoint: subscription.endpoint,
auth: encode(subscription.getKey('auth')),
publickey: encode(subscription.getKey('p256dh')),
@@ -102,12 +103,12 @@ function subscribe() {
}
async function unsubscribe() {
- if (!pushSubscription) return;
+ if (!pushSubscription.value) return;
- const endpoint = pushSubscription.endpoint;
+ const endpoint = pushSubscription.value.endpoint;
const accounts = await getAccounts();
- pushRegistrationInServer = undefined;
+ pushRegistrationInServer.value = undefined;
if ($i && accounts.length >= 2) {
apiWithDialog('sw/unregister', {
@@ -115,11 +116,11 @@ async function unsubscribe() {
endpoint,
});
} else {
- pushSubscription.unsubscribe();
+ pushSubscription.value.unsubscribe();
apiWithDialog('sw/unregister', {
endpoint,
});
- pushSubscription = null;
+ pushSubscription.value = null;
}
}
@@ -150,20 +151,20 @@ if (navigator.serviceWorker == null) {
// TODO: よしなに?
} else {
navigator.serviceWorker.ready.then(async swr => {
- registration = swr;
+ registration.value = swr;
- pushSubscription = await registration.pushManager.getSubscription();
+ pushSubscription.value = await registration.value.pushManager.getSubscription();
if (instance.swPublickey && ('PushManager' in window) && $i && $i.token) {
- supported = true;
+ supported.value = true;
- if (pushSubscription) {
+ if (pushSubscription.value) {
const res = await api('sw/show-registration', {
- endpoint: pushSubscription.endpoint,
+ endpoint: pushSubscription.value.endpoint,
});
if (res) {
- pushRegistrationInServer = res;
+ pushRegistrationInServer.value = res;
}
}
}
@@ -171,6 +172,6 @@ if (navigator.serviceWorker == null) {
}
defineExpose({
- pushRegistrationInServer: $$(pushRegistrationInServer),
+ pushRegistrationInServer: pushRegistrationInServer,
});
</script>
diff --git a/packages/frontend/src/components/MkRadio.vue b/packages/frontend/src/components/MkRadio.vue
index f22774ef97..edb3abe5f7 100644
--- a/packages/frontend/src/components/MkRadio.vue
+++ b/packages/frontend/src/components/MkRadio.vue
@@ -24,7 +24,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { } from 'vue';
+import { computed } from 'vue';
const props = defineProps<{
modelValue: any;
@@ -36,7 +36,7 @@ const emit = defineEmits<{
(ev: 'update:modelValue', value: any): void;
}>();
-let checked = $computed(() => props.modelValue === props.value);
+const checked = computed(() => props.modelValue === props.value);
function toggle(): void {
if (props.disabled) return;
diff --git a/packages/frontend/src/components/MkReactionEffect.vue b/packages/frontend/src/components/MkReactionEffect.vue
index 88e262d880..75eb91e7ad 100644
--- a/packages/frontend/src/components/MkReactionEffect.vue
+++ b/packages/frontend/src/components/MkReactionEffect.vue
@@ -12,7 +12,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { onMounted } from 'vue';
+import { onMounted, ref } from 'vue';
import * as os from '@/os.js';
import MkReactionIcon from '@/components/MkReactionIcon.vue';
@@ -27,13 +27,13 @@ const emit = defineEmits<{
(ev: 'end'): void;
}>();
-let up = $ref(false);
+const up = ref(false);
const zIndex = os.claimZIndex('middle');
const angle = (90 - (Math.random() * 180)) + 'deg';
onMounted(() => {
window.setTimeout(() => {
- up = true;
+ up.value = true;
}, 10);
window.setTimeout(() => {
diff --git a/packages/frontend/src/components/MkReactionsViewer.reaction.vue b/packages/frontend/src/components/MkReactionsViewer.reaction.vue
index 42b5243e94..e7901316a2 100644
--- a/packages/frontend/src/components/MkReactionsViewer.reaction.vue
+++ b/packages/frontend/src/components/MkReactionsViewer.reaction.vue
@@ -11,7 +11,7 @@ SPDX-License-Identifier: AGPL-3.0-only
:class="[$style.root, { [$style.reacted]: note.myReaction == reaction, [$style.canToggle]: canToggle, [$style.small]: defaultStore.state.reactionsDisplaySize === 'small', [$style.large]: defaultStore.state.reactionsDisplaySize === 'large' }]"
@click="toggleReaction()"
>
- <MkReactionIcon :class="$style.icon" :reaction="reaction" :emojiUrl="note.reactionEmojis[reaction.substring(1, reaction.length - 1)]" @click="toggleReaction()"/>
+ <MkReactionIcon :class="defaultStore.state.limitWidthOfReaction ? $style.limitWidth : ''" :reaction="reaction" :emojiUrl="note.reactionEmojis[reaction.substring(1, reaction.length - 1)]" @click="toggleReaction()"/>
<span :class="$style.count">{{ count }}</span>
</button>
</template>
@@ -28,6 +28,7 @@ import MkReactionEffect from '@/components/MkReactionEffect.vue';
import { claimAchievement } from '@/scripts/achievements.js';
import { defaultStore } from '@/store.js';
import { i18n } from '@/i18n.js';
+import * as sound from '@/scripts/sound.js';
const props = defineProps<{
reaction: string;
@@ -59,6 +60,10 @@ async function toggleReaction() {
});
if (confirm.canceled) return;
+ if (oldReaction !== props.reaction) {
+ sound.play('reaction');
+ }
+
if (mock) {
emit('reactionToggled', props.reaction, (props.count - 1));
return;
@@ -75,6 +80,8 @@ async function toggleReaction() {
}
});
} else {
+ sound.play('reaction');
+
if (mock) {
emit('reactionToggled', props.reaction, (props.count + 1));
return;
@@ -132,12 +139,14 @@ if (!mock) {
<style lang="scss" module>
.root {
- display: inline-block;
+ display: inline-flex;
height: 42px;
margin: 2px;
padding: 0 6px;
font-size: 1.5em;
border-radius: var(--radius-sm);
+ align-items: center;
+ justify-content: center;
&.canToggle {
background: var(--buttonBg);
@@ -176,7 +185,7 @@ if (!mock) {
&.reacted, &.reacted:hover {
background: var(--accentedBg);
color: var(--accent);
- box-shadow: 0 0 0px 1px var(--accent) inset;
+ box-shadow: 0 0 0 1px var(--accent) inset;
> .count {
color: var(--accent);
@@ -188,7 +197,7 @@ if (!mock) {
}
}
-.icon {
+.limitWidth {
max-width: 150px;
}
diff --git a/packages/frontend/src/components/MkReactionsViewer.vue b/packages/frontend/src/components/MkReactionsViewer.vue
index 13d022977e..d2a5c431fe 100644
--- a/packages/frontend/src/components/MkReactionsViewer.vue
+++ b/packages/frontend/src/components/MkReactionsViewer.vue
@@ -19,7 +19,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import * as Misskey from 'misskey-js';
-import { inject, watch } from 'vue';
+import { inject, watch, ref } from 'vue';
import XReaction from '@/components/MkReactionsViewer.reaction.vue';
import { defaultStore } from '@/store.js';
@@ -38,31 +38,31 @@ const emit = defineEmits<{
const initialReactions = new Set(Object.keys(props.note.reactions));
-let reactions = $ref<[string, number][]>([]);
-let hasMoreReactions = $ref(false);
+const reactions = ref<[string, number][]>([]);
+const hasMoreReactions = ref(false);
-if (props.note.myReaction && !Object.keys(reactions).includes(props.note.myReaction)) {
- reactions[props.note.myReaction] = props.note.reactions[props.note.myReaction];
+if (props.note.myReaction && !Object.keys(reactions.value).includes(props.note.myReaction)) {
+ reactions.value[props.note.myReaction] = props.note.reactions[props.note.myReaction];
}
function onMockToggleReaction(emoji: string, count: number) {
if (!mock) return;
- const i = reactions.findIndex((item) => item[0] === emoji);
+ const i = reactions.value.findIndex((item) => item[0] === emoji);
if (i < 0) return;
- emit('mockUpdateMyReaction', emoji, (count - reactions[i][1]));
+ emit('mockUpdateMyReaction', emoji, (count - reactions.value[i][1]));
}
watch([() => props.note.reactions, () => props.maxNumber], ([newSource, maxNumber]) => {
let newReactions: [string, number][] = [];
- hasMoreReactions = Object.keys(newSource).length > maxNumber;
+ hasMoreReactions.value = Object.keys(newSource).length > maxNumber;
- for (let i = 0; i < reactions.length; i++) {
- const reaction = reactions[i][0];
+ for (let i = 0; i < reactions.value.length; i++) {
+ const reaction = reactions.value[i][0];
if (reaction in newSource && newSource[reaction] !== 0) {
- reactions[i][1] = newSource[reaction];
- newReactions.push(reactions[i]);
+ reactions.value[i][1] = newSource[reaction];
+ newReactions.push(reactions.value[i]);
}
}
@@ -80,7 +80,7 @@ watch([() => props.note.reactions, () => props.maxNumber], ([newSource, maxNumbe
newReactions.push([props.note.myReaction, newSource[props.note.myReaction]]);
}
- reactions = newReactions;
+ reactions.value = newReactions;
}, { immediate: true, deep: true });
</script>
diff --git a/packages/frontend/src/components/MkRetentionHeatmap.vue b/packages/frontend/src/components/MkRetentionHeatmap.vue
index 3dc9a94ae2..e69aa1be80 100644
--- a/packages/frontend/src/components/MkRetentionHeatmap.vue
+++ b/packages/frontend/src/components/MkRetentionHeatmap.vue
@@ -13,7 +13,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { onMounted, nextTick } from 'vue';
+import { onMounted, nextTick, shallowRef, ref } from 'vue';
import { Chart } from 'chart.js';
import * as os from '@/os.js';
import { defaultStore } from '@/store.js';
@@ -23,11 +23,11 @@ import { initChart } from '@/scripts/init-chart.js';
initChart();
-const rootEl = $shallowRef<HTMLDivElement>(null);
-const chartEl = $shallowRef<HTMLCanvasElement>(null);
+const rootEl = shallowRef<HTMLDivElement>(null);
+const chartEl = shallowRef<HTMLCanvasElement>(null);
const now = new Date();
let chartInstance: Chart = null;
-let fetching = $ref(true);
+const fetching = ref(true);
const { handler: externalTooltipHandler } = useChartTooltip({
position: 'middle',
@@ -38,8 +38,8 @@ async function renderChart() {
chartInstance.destroy();
}
- const wide = rootEl.offsetWidth > 600;
- const narrow = rootEl.offsetWidth < 400;
+ const wide = rootEl.value.offsetWidth > 600;
+ const narrow = rootEl.value.offsetWidth < 400;
const maxDays = wide ? 10 : narrow ? 5 : 7;
@@ -66,7 +66,7 @@ async function renderChart() {
}
}
- fetching = false;
+ fetching.value = false;
await nextTick();
@@ -83,7 +83,7 @@ async function renderChart() {
const marginEachCell = 12;
- chartInstance = new Chart(chartEl, {
+ chartInstance = new Chart(chartEl.value, {
type: 'matrix',
data: {
datasets: [{
diff --git a/packages/frontend/src/components/MkSignin.vue b/packages/frontend/src/components/MkSignin.vue
index a8718b98d6..08830fca7a 100644
--- a/packages/frontend/src/components/MkSignin.vue
+++ b/packages/frontend/src/components/MkSignin.vue
@@ -49,7 +49,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { defineAsyncComponent } from 'vue';
+import { defineAsyncComponent, ref } from 'vue';
import { toUnicode } from 'punycode/';
import * as Misskey from 'misskey-js';
import { supported as webAuthnSupported, get as webAuthnRequest, parseRequestOptionsFromJSON } from '@github/webauthn-json/browser-ponyfill';
@@ -62,17 +62,17 @@ import * as os from '@/os.js';
import { login } from '@/account.js';
import { i18n } from '@/i18n.js';
-let signing = $ref(false);
-let user = $ref<Misskey.entities.UserDetailed | null>(null);
-let username = $ref('');
-let password = $ref('');
-let token = $ref('');
-let host = $ref(toUnicode(configHost));
-let totpLogin = $ref(false);
-let queryingKey = $ref(false);
-let credentialRequest = $ref<CredentialRequestOptions | null>(null);
-let hCaptchaResponse = $ref(null);
-let reCaptchaResponse = $ref(null);
+const signing = ref(false);
+const user = ref<Misskey.entities.UserDetailed | null>(null);
+const username = ref('');
+const password = ref('');
+const token = ref('');
+const host = ref(toUnicode(configHost));
+const totpLogin = ref(false);
+const queryingKey = ref(false);
+const credentialRequest = ref<CredentialRequestOptions | null>(null);
+const hCaptchaResponse = ref(null);
+const reCaptchaResponse = ref(null);
const emit = defineEmits<{
(ev: 'login', v: any): void;
@@ -98,11 +98,11 @@ const props = defineProps({
function onUsernameChange(): void {
os.api('users/show', {
- username: username,
+ username: username.value,
}).then(userResponse => {
- user = userResponse;
+ user.value = userResponse;
}, () => {
- user = null;
+ user.value = null;
});
}
@@ -113,21 +113,21 @@ function onLogin(res: any): Promise<void> | void {
}
async function queryKey(): Promise<void> {
- queryingKey = true;
- await webAuthnRequest(credentialRequest)
+ queryingKey.value = true;
+ await webAuthnRequest(credentialRequest.value)
.catch(() => {
- queryingKey = false;
+ queryingKey.value = false;
return Promise.reject(null);
}).then(credential => {
- credentialRequest = null;
- queryingKey = false;
- signing = true;
+ credentialRequest.value = null;
+ queryingKey.value = false;
+ signing.value = true;
return os.api('signin', {
- username,
- password,
+ username: username.value,
+ password: password.value,
credential: credential.toJSON(),
- 'hcaptcha-response': hCaptchaResponse,
- 'g-recaptcha-response': reCaptchaResponse,
+ 'hcaptcha-response': hCaptchaResponse.value,
+ 'g-recaptcha-response': reCaptchaResponse.value,
});
}).then(res => {
emit('login', res);
@@ -138,39 +138,39 @@ async function queryKey(): Promise<void> {
type: 'error',
text: i18n.ts.signinFailed,
});
- signing = false;
+ signing.value = false;
});
}
function onSubmit(): void {
- signing = true;
- if (!totpLogin && user && user.twoFactorEnabled) {
- if (webAuthnSupported() && user.securityKeys) {
+ signing.value = true;
+ if (!totpLogin.value && user.value && user.value.twoFactorEnabled) {
+ if (webAuthnSupported() && user.value.securityKeys) {
os.api('signin', {
- username,
- password,
- 'hcaptcha-response': hCaptchaResponse,
- 'g-recaptcha-response': reCaptchaResponse,
+ username: username.value,
+ password: password.value,
+ 'hcaptcha-response': hCaptchaResponse.value,
+ 'g-recaptcha-response': reCaptchaResponse.value,
}).then(res => {
- totpLogin = true;
- signing = false;
- credentialRequest = parseRequestOptionsFromJSON({
+ totpLogin.value = true;
+ signing.value = false;
+ credentialRequest.value = parseRequestOptionsFromJSON({
publicKey: res,
});
})
.then(() => queryKey())
.catch(loginFailed);
} else {
- totpLogin = true;
- signing = false;
+ totpLogin.value = true;
+ signing.value = false;
}
} else {
os.api('signin', {
- username,
- password,
- 'hcaptcha-response': hCaptchaResponse,
- 'g-recaptcha-response': reCaptchaResponse,
- token: user?.twoFactorEnabled ? token : undefined,
+ username: username.value,
+ password: password.value,
+ 'hcaptcha-response': hCaptchaResponse.value,
+ 'g-recaptcha-response': reCaptchaResponse.value,
+ token: user.value?.twoFactorEnabled ? token.value : undefined,
}).then(res => {
emit('login', res);
onLogin(res);
@@ -218,8 +218,8 @@ function loginFailed(err: any): void {
}
}
- totpLogin = false;
- signing = false;
+ totpLogin.value = false;
+ signing.value = false;
}
function resetPassword(): void {
diff --git a/packages/frontend/src/components/MkSigninDialog.vue b/packages/frontend/src/components/MkSigninDialog.vue
index 05cef6ed3b..6f961cff05 100644
--- a/packages/frontend/src/components/MkSigninDialog.vue
+++ b/packages/frontend/src/components/MkSigninDialog.vue
@@ -20,7 +20,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { } from 'vue';
+import { shallowRef } from 'vue';
import MkSignin from '@/components/MkSignin.vue';
import MkModalWindow from '@/components/MkModalWindow.vue';
import { i18n } from '@/i18n.js';
@@ -39,15 +39,15 @@ const emit = defineEmits<{
(ev: 'cancelled'): void;
}>();
-const dialog = $shallowRef<InstanceType<typeof MkModalWindow>>();
+const dialog = shallowRef<InstanceType<typeof MkModalWindow>>();
function onClose() {
emit('cancelled');
- if (dialog) dialog.close();
+ if (dialog.value) dialog.value.close();
}
function onLogin(res) {
emit('done', res);
- if (dialog) dialog.close();
+ if (dialog.value) dialog.value.close();
}
</script>
diff --git a/packages/frontend/src/components/MkSignupDialog.form.vue b/packages/frontend/src/components/MkSignupDialog.form.vue
index 389acb82bc..b46dc4bd93 100644
--- a/packages/frontend/src/components/MkSignupDialog.form.vue
+++ b/packages/frontend/src/components/MkSignupDialog.form.vue
@@ -80,11 +80,10 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { } from 'vue';
+import { ref, computed } from 'vue';
import { toUnicode } from 'punycode/';
import MkButton from './MkButton.vue';
import MkInput from './MkInput.vue';
-import MkSwitch from './MkSwitch.vue';
import MkCaptcha, { type Captcha } from '@/components/MkCaptcha.vue';
import * as config from '@/config.js';
import * as os from '@/os.js';
@@ -106,35 +105,35 @@ const emit = defineEmits<{
const host = toUnicode(config.host);
-let hcaptcha = $ref<Captcha | undefined>();
-let recaptcha = $ref<Captcha | undefined>();
-let turnstile = $ref<Captcha | undefined>();
+const hcaptcha = ref<Captcha | undefined>();
+const recaptcha = ref<Captcha | undefined>();
+const turnstile = ref<Captcha | undefined>();
-let username: string = $ref('');
-let password: string = $ref('');
-let retypedPassword: string = $ref('');
-let invitationCode: string = $ref('');
-let reason: string = $ref('');
-let email = $ref('');
-let usernameState: null | 'wait' | 'ok' | 'unavailable' | 'error' | 'invalid-format' | 'min-range' | 'max-range' = $ref(null);
-let emailState: null | 'wait' | 'ok' | 'unavailable:used' | 'unavailable:format' | 'unavailable:disposable' | 'unavailable:mx' | 'unavailable:smtp' | 'unavailable' | 'error' = $ref(null);
-let passwordStrength: '' | 'low' | 'medium' | 'high' = $ref('');
-let passwordRetypeState: null | 'match' | 'not-match' = $ref(null);
-let submitting: boolean = $ref(false);
-let hCaptchaResponse = $ref(null);
-let reCaptchaResponse = $ref(null);
-let turnstileResponse = $ref(null);
-let usernameAbortController: null | AbortController = $ref(null);
-let emailAbortController: null | AbortController = $ref(null);
+const username = ref<string>('');
+const password = ref<string>('');
+const retypedPassword = ref<string>('');
+const invitationCode = ref<string>('');
+const reason = ref<string>('');
+const email = ref('');
+const usernameState = ref<null | 'wait' | 'ok' | 'unavailable' | 'error' | 'invalid-format' | 'min-range' | 'max-range'>(null);
+const emailState = ref<null | 'wait' | 'ok' | 'unavailable:used' | 'unavailable:format' | 'unavailable:disposable' | 'unavailable:mx' | 'unavailable:smtp' | 'unavailable' | 'error'>(null);
+const passwordStrength = ref<'' | 'low' | 'medium' | 'high'>('');
+const passwordRetypeState = ref<null | 'match' | 'not-match'>(null);
+const submitting = ref<boolean>(false);
+const hCaptchaResponse = ref(null);
+const reCaptchaResponse = ref(null);
+const turnstileResponse = ref(null);
+const usernameAbortController = ref<null | AbortController>(null);
+const emailAbortController = ref<null | AbortController>(null);
-const shouldDisableSubmitting = $computed((): boolean => {
- return submitting ||
- instance.enableHcaptcha && !hCaptchaResponse ||
- instance.enableRecaptcha && !reCaptchaResponse ||
- instance.enableTurnstile && !turnstileResponse ||
- instance.emailRequiredForSignup && emailState !== 'ok' ||
- usernameState !== 'ok' ||
- passwordRetypeState !== 'match';
+const shouldDisableSubmitting = computed((): boolean => {
+ return submitting.value ||
+ instance.enableHcaptcha && !hCaptchaResponse.value ||
+ instance.enableRecaptcha && !reCaptchaResponse.value ||
+ instance.enableTurnstile && !turnstileResponse.value ||
+ instance.emailRequiredForSignup && emailState.value !== 'ok' ||
+ usernameState.value !== 'ok' ||
+ passwordRetypeState.value !== 'match';
});
function getPasswordStrength(source: string): number {
@@ -162,57 +161,57 @@ function getPasswordStrength(source: string): number {
}
function onChangeUsername(): void {
- if (username === '') {
- usernameState = null;
+ if (username.value === '') {
+ usernameState.value = null;
return;
}
{
const err =
- !username.match(/^[a-zA-Z0-9_]+$/) ? 'invalid-format' :
- username.length < 1 ? 'min-range' :
- username.length > 20 ? 'max-range' :
+ !username.value.match(/^[a-zA-Z0-9_]+$/) ? 'invalid-format' :
+ username.value.length < 1 ? 'min-range' :
+ username.value.length > 20 ? 'max-range' :
null;
if (err) {
- usernameState = err;
+ usernameState.value = err;
return;
}
}
- if (usernameAbortController != null) {
- usernameAbortController.abort();
+ if (usernameAbortController.value != null) {
+ usernameAbortController.value.abort();
}
- usernameState = 'wait';
- usernameAbortController = new AbortController();
+ usernameState.value = 'wait';
+ usernameAbortController.value = new AbortController();
os.api('username/available', {
- username,
- }, undefined, usernameAbortController.signal).then(result => {
- usernameState = result.available ? 'ok' : 'unavailable';
+ username: username.value,
+ }, undefined, usernameAbortController.value.signal).then(result => {
+ usernameState.value = result.available ? 'ok' : 'unavailable';
}).catch((err) => {
if (err.name !== 'AbortError') {
- usernameState = 'error';
+ usernameState.value = 'error';
}
});
}
function onChangeEmail(): void {
- if (email === '') {
- emailState = null;
+ if (email.value === '') {
+ emailState.value = null;
return;
}
- if (emailAbortController != null) {
- emailAbortController.abort();
+ if (emailAbortController.value != null) {
+ emailAbortController.value.abort();
}
- emailState = 'wait';
- emailAbortController = new AbortController();
+ emailState.value = 'wait';
+ emailAbortController.value = new AbortController();
os.api('email-address/available', {
- emailAddress: email,
- }, undefined, emailAbortController.signal).then(result => {
- emailState = result.available ? 'ok' :
+ emailAddress: email.value,
+ }, undefined, emailAbortController.value.signal).then(result => {
+ emailState.value = result.available ? 'ok' :
result.reason === 'used' ? 'unavailable:used' :
result.reason === 'format' ? 'unavailable:format' :
result.reason === 'disposable' ? 'unavailable:disposable' :
@@ -221,50 +220,49 @@ function onChangeEmail(): void {
'unavailable';
}).catch((err) => {
if (err.name !== 'AbortError') {
- emailState = 'error';
+ emailState.value = 'error';
}
});
}
function onChangePassword(): void {
- if (password === '') {
- passwordStrength = '';
+ if (password.value === '') {
+ passwordStrength.value = '';
return;
}
- const strength = getPasswordStrength(password);
- passwordStrength = strength > 0.7 ? 'high' : strength > 0.3 ? 'medium' : 'low';
+ const strength = getPasswordStrength(password.value);
+ passwordStrength.value = strength > 0.7 ? 'high' : strength > 0.3 ? 'medium' : 'low';
}
function onChangePasswordRetype(): void {
- if (retypedPassword === '') {
- passwordRetypeState = null;
+ if (retypedPassword.value === '') {
+ passwordRetypeState.value = null;
return;
}
- passwordRetypeState = password === retypedPassword ? 'match' : 'not-match';
+ passwordRetypeState.value = password.value === retypedPassword.value ? 'match' : 'not-match';
}
async function onSubmit(): Promise<void> {
- if (submitting) return;
- submitting = true;
+ if (submitting.value) return;
+ submitting.value = true;
try {
await os.api('signup', {
- username,
- password,
- emailAddress: email,
- invitationCode,
- reason,
- 'hcaptcha-response': hCaptchaResponse,
- 'g-recaptcha-response': reCaptchaResponse,
- 'turnstile-response': turnstileResponse,
+ username: username.value,
+ password: password.value,
+ emailAddress: email.value,
+ invitationCode: invitationCode.value,
+ reason: reason.value,
+ 'hcaptcha-response': hCaptchaResponse.value,
+ 'g-recaptcha-response': reCaptchaResponse.value,
});
if (instance.emailRequiredForSignup) {
os.alert({
type: 'success',
title: i18n.ts._signup.almostThere,
- text: i18n.t('_signup.emailSent', { email }),
+ text: i18n.t('_signup.emailSent', { email: email.value }),
});
emit('signupEmailPending');
} else if (instance.approvalRequiredForSignup) {
@@ -276,8 +274,8 @@ async function onSubmit(): Promise<void> {
emit('approvalPending');
} else {
const res = await os.api('signin', {
- username,
- password,
+ username: username.value,
+ password: password.value,
});
emit('signup', res);
@@ -286,10 +284,10 @@ async function onSubmit(): Promise<void> {
}
}
} catch {
- submitting = false;
- hcaptcha?.reset?.();
- recaptcha?.reset?.();
- turnstile?.reset?.();
+ submitting.value = false;
+ hcaptcha.value?.reset?.();
+ recaptcha.value?.reset?.();
+ turnstile.value?.reset?.();
os.alert({
type: 'error',
diff --git a/packages/frontend/src/components/MkSignupDialog.rules.vue b/packages/frontend/src/components/MkSignupDialog.rules.vue
index 09eac0732a..bc4fec305b 100644
--- a/packages/frontend/src/components/MkSignupDialog.rules.vue
+++ b/packages/frontend/src/components/MkSignupDialog.rules.vue
@@ -62,7 +62,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { computed, onMounted, ref, watch } from 'vue';
+import { computed, ref } from 'vue';
import { instance } from '@/instance.js';
import { i18n } from '@/i18n.js';
import MkButton from '@/components/MkButton.vue';
@@ -96,7 +96,7 @@ const tosPrivacyPolicyLabel = computed(() => {
} else if (availablePrivacyPolicy) {
return i18n.ts.privacyPolicy;
} else {
- return "";
+ return '';
}
});
diff --git a/packages/frontend/src/components/MkSignupDialog.vue b/packages/frontend/src/components/MkSignupDialog.vue
index 73d3b644e9..c8020c6636 100644
--- a/packages/frontend/src/components/MkSignupDialog.vue
+++ b/packages/frontend/src/components/MkSignupDialog.vue
@@ -33,13 +33,12 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { } from 'vue';
-import { $ref } from 'vue/macros';
+import { shallowRef, ref } from 'vue';
+
import XSignup from '@/components/MkSignupDialog.form.vue';
import XServerRules from '@/components/MkSignupDialog.rules.vue';
import MkModalWindow from '@/components/MkModalWindow.vue';
import { i18n } from '@/i18n.js';
-import { instance } from '@/instance.js';
const props = withDefaults(defineProps<{
autoSet?: boolean;
@@ -52,17 +51,17 @@ const emit = defineEmits<{
(ev: 'closed'): void;
}>();
-const dialog = $shallowRef<InstanceType<typeof MkModalWindow>>();
+const dialog = shallowRef<InstanceType<typeof MkModalWindow>>();
-const isAcceptedServerRule = $ref(false);
+const isAcceptedServerRule = ref(false);
function onSignup(res) {
emit('done', res);
- dialog.close();
+ dialog.value.close();
}
function onSignupEmailPending() {
- dialog.close();
+ dialog.value.close();
}
function onApprovalPending() {
diff --git a/packages/frontend/src/components/MkSubNoteContent.vue b/packages/frontend/src/components/MkSubNoteContent.vue
index a91f1f444c..c071fb938a 100644
--- a/packages/frontend/src/components/MkSubNoteContent.vue
+++ b/packages/frontend/src/components/MkSubNoteContent.vue
@@ -8,10 +8,10 @@ SPDX-License-Identifier: AGPL-3.0-only
<div :class="{ [$style.clickToOpen]: defaultStore.state.clickToOpen }" @click="defaultStore.state.clickToOpen ? noteclick(note.id) : undefined">
<span v-if="note.isHidden" style="opacity: 0.5">({{ i18n.ts.private }})</span>
<span v-if="note.deletedAt" style="opacity: 0.5">({{ i18n.ts.deleted }})</span>
- <MkA v-if="note.replyId" :class="$style.reply" :to="`/notes/${note.replyId}`" v-on:click.stop><i class="ph-arrow-bend-left-up ph-bold ph-lg"></i></MkA>
+ <MkA v-if="note.replyId" :class="$style.reply" :to="`/notes/${note.replyId}`" @click.stop><i class="ph-arrow-bend-left-up ph-bold ph-lg"></i></MkA>
<Mfm v-if="note.text" :text="note.text" :author="note.user" :nyaize="'respect'" :isAnim="allowAnim" :emojiUrls="note.emojis"/>
- <MkButton v-if="!allowAnim && animated && !hideFiles" :class="$style.playMFMButton" :small="true" @click="animatedMFM()" v-on:click.stop><i class="ph-play ph-bold ph-lg "></i> {{ i18n.ts._animatedMFM.play }}</MkButton>
- <MkButton v-else-if="!defaultStore.state.animatedMfm && allowAnim && animated && !hideFiles" :class="$style.playMFMButton" :small="true" @click="animatedMFM()" v-on:click.stop><i class="ph-stop ph-bold ph-lg "></i> {{ i18n.ts._animatedMFM.stop }}</MkButton>
+ <MkButton v-if="!allowAnim && animated && !hideFiles" :class="$style.playMFMButton" :small="true" @click="animatedMFM()" @click.stop><i class="ph-play ph-bold ph-lg "></i> {{ i18n.ts._animatedMFM.play }}</MkButton>
+ <MkButton v-else-if="!defaultStore.state.animatedMfm && allowAnim && animated && !hideFiles" :class="$style.playMFMButton" :small="true" @click="animatedMFM()" @click.stop><i class="ph-stop ph-bold ph-lg "></i> {{ i18n.ts._animatedMFM.stop }}</MkButton>
<div v-if="note.text && translating || note.text && translation" :class="$style.translation">
<MkLoading v-if="translating" mini/>
<div v-else>
@@ -19,7 +19,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<Mfm :text="translation.text" :author="note.user" :nyaize="'respect'" :emojiUrls="note.emojis"/>
</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}`" @click.stop>RN: ...</MkA>
</div>
<details v-if="note.files.length > 0" :open="!defaultStore.state.collapseFiles && !hideFiles">
<summary>({{ i18n.t('withNFiles', { n: note.files.length }) }})</summary>
@@ -39,14 +39,13 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { } from 'vue';
+import { ref, computed } from 'vue';
import * as Misskey from 'misskey-js';
import * as mfm from '@sharkey/sfm-js';
import MkMediaList from '@/components/MkMediaList.vue';
import MkPoll from '@/components/MkPoll.vue';
import MkButton from '@/components/MkButton.vue';
import { i18n } from '@/i18n.js';
-import { $i } from '@/account.js';
import { shouldCollapsed } from '@/scripts/collapsed.js';
import { defaultStore } from '@/store.js';
import { useRouter } from '@/router.js';
@@ -69,25 +68,25 @@ function noteclick(id: string) {
}
}
-const parsed = $computed(() => props.note.text ? mfm.parse(props.note.text) : null);
-const animated = $computed(() => parsed ? checkAnimationFromMfm(parsed) : null);
-let allowAnim = $ref(defaultStore.state.advancedMfm && defaultStore.state.animatedMfm ? true : false);
+const parsed = computed(() => props.note.text ? mfm.parse(props.note.text) : null);
+const animated = computed(() => parsed.value ? checkAnimationFromMfm(parsed.value) : null);
+let allowAnim = ref(defaultStore.state.advancedMfm && defaultStore.state.animatedMfm ? true : false);
const isLong = defaultStore.state.expandLongNote && !props.hideFiles ? false : shouldCollapsed(props.note, []);
function animatedMFM() {
- if (allowAnim) {
- allowAnim = false;
+ if (allowAnim.value) {
+ allowAnim.value = false;
} else {
os.confirm({
type: 'warning',
text: i18n.ts._animatedMFM._alert.text,
okText: i18n.ts._animatedMFM._alert.confirm,
- }).then((res) => { if (!res.canceled) allowAnim = true; });
+ }).then((res) => { if (!res.canceled) allowAnim.value = true; });
}
}
-const collapsed = $ref(isLong);
+const collapsed = ref(isLong);
</script>
<style lang="scss" module>
diff --git a/packages/frontend/src/components/MkSwitch.vue b/packages/frontend/src/components/MkSwitch.vue
index 7521bd6c76..35e5aebbdd 100644
--- a/packages/frontend/src/components/MkSwitch.vue
+++ b/packages/frontend/src/components/MkSwitch.vue
@@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
-<div :class="[$style.root, { [$style.disabled]: disabled, [$style.checked]: checked }]">
+<div :class="[$style.root, { [$style.disabled]: disabled }]">
<input
ref="input"
type="checkbox"
@@ -64,9 +64,6 @@ const toggle = () => {
opacity: 0.6;
cursor: not-allowed;
}
-
- //&.checked {
- //}
}
.input {
diff --git a/packages/frontend/src/components/MkTagCloud.vue b/packages/frontend/src/components/MkTagCloud.vue
index a3d82fee5e..083c34906f 100644
--- a/packages/frontend/src/components/MkTagCloud.vue
+++ b/packages/frontend/src/components/MkTagCloud.vue
@@ -15,7 +15,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { onMounted, watch, onBeforeUnmount } from 'vue';
+import { onMounted, watch, onBeforeUnmount, ref, shallowRef } from 'vue';
import tinycolor from 'tinycolor2';
const loaded = !!window.TagCanvas;
@@ -23,13 +23,13 @@ const SAFE_FOR_HTML_ID = 'abcdefghijklmnopqrstuvwxyz';
const computedStyle = getComputedStyle(document.documentElement);
const idForCanvas = Array.from({ length: 16 }, () => SAFE_FOR_HTML_ID[Math.floor(Math.random() * SAFE_FOR_HTML_ID.length)]).join('');
const idForTags = Array.from({ length: 16 }, () => SAFE_FOR_HTML_ID[Math.floor(Math.random() * SAFE_FOR_HTML_ID.length)]).join('');
-let available = $ref(false);
-let rootEl = $shallowRef<HTMLElement | null>(null);
-let canvasEl = $shallowRef<HTMLCanvasElement | null>(null);
-let tagsEl = $shallowRef<HTMLElement | null>(null);
-let width = $ref(300);
+const available = ref(false);
+const rootEl = shallowRef<HTMLElement | null>(null);
+const canvasEl = shallowRef<HTMLCanvasElement | null>(null);
+const tagsEl = shallowRef<HTMLElement | null>(null);
+const width = ref(300);
-watch($$(available), () => {
+watch(available, () => {
try {
window.TagCanvas.Start(idForCanvas, idForTags, {
textColour: '#ffffff',
@@ -52,15 +52,15 @@ watch($$(available), () => {
});
onMounted(() => {
- width = rootEl.offsetWidth;
+ width.value = rootEl.value.offsetWidth;
if (loaded) {
- available = true;
+ available.value = true;
} else {
document.head.appendChild(Object.assign(document.createElement('script'), {
async: true,
src: '/client-assets/tagcanvas.min.js',
- })).addEventListener('load', () => available = true);
+ })).addEventListener('load', () => available.value = true);
}
});
diff --git a/packages/frontend/src/components/MkTextarea.vue b/packages/frontend/src/components/MkTextarea.vue
index c35274959e..5c70adde11 100644
--- a/packages/frontend/src/components/MkTextarea.vue
+++ b/packages/frontend/src/components/MkTextarea.vue
@@ -17,7 +17,7 @@ SPDX-License-Identifier: AGPL-3.0-only
:readonly="readonly"
:placeholder="placeholder"
:pattern="pattern"
- :autocomplete="autocomplete"
+ :autocomplete="props.autocomplete"
:spellcheck="spellcheck"
@focus="focused = true"
@blur="focused = false"
@@ -26,16 +26,21 @@ SPDX-License-Identifier: AGPL-3.0-only
></textarea>
</div>
<div :class="$style.caption"><slot name="caption"></slot></div>
+ <button v-if="mfmPreview" style="font-size: 0.85em;" class="_textButton" type="button" @click="preview = !preview">{{ i18n.ts.preview }}</button>
+ <div v-if="mfmPreview" v-show="preview" v-panel :class="$style.mfmPreview">
+ <Mfm :text="v"/>
+ </div>
<MkButton v-if="manualSave && changed" primary :class="$style.save" @click="updated"><i class="ph-floppy-disk ph-bold ph-lg"></i> {{ i18n.ts.save }}</MkButton>
</div>
</template>
<script lang="ts" setup>
-import { onMounted, nextTick, ref, watch, computed, toRefs, shallowRef } from 'vue';
+import { onMounted, onUnmounted, nextTick, ref, watch, computed, toRefs, shallowRef } from 'vue';
import { debounce } from 'throttle-debounce';
import MkButton from '@/components/MkButton.vue';
import { i18n } from '@/i18n.js';
+import { Autocomplete, SuggestionType } from '@/scripts/autocomplete.js';
const props = defineProps<{
modelValue: string | null;
@@ -46,6 +51,8 @@ const props = defineProps<{
placeholder?: string;
autofocus?: boolean;
autocomplete?: string;
+ mfmAutocomplete?: boolean | SuggestionType[],
+ mfmPreview?: boolean;
spellcheck?: boolean;
debounce?: boolean;
manualSave?: boolean;
@@ -68,6 +75,8 @@ const changed = ref(false);
const invalid = ref(false);
const filled = computed(() => v.value !== '' && v.value != null);
const inputEl = shallowRef<HTMLTextAreaElement>();
+const preview = ref(false);
+let autocomplete: Autocomplete;
const focus = () => inputEl.value.focus();
const onInput = (ev) => {
@@ -82,6 +91,16 @@ const onKeydown = (ev: KeyboardEvent) => {
if (ev.code === 'Enter') {
emit('enter');
}
+
+ if (props.code && ev.key === 'Tab') {
+ const pos = inputEl.value?.selectionStart ?? 0;
+ const posEnd = inputEl.value?.selectionEnd ?? v.value.length;
+ v.value = v.value.slice(0, pos) + '\t' + v.value.slice(posEnd);
+ nextTick(() => {
+ inputEl.value?.setSelectionRange(pos + 1, pos + 1);
+ });
+ ev.preventDefault();
+ }
};
const updated = () => {
@@ -113,6 +132,16 @@ onMounted(() => {
focus();
}
});
+
+ if (props.mfmAutocomplete) {
+ autocomplete = new Autocomplete(inputEl.value, v, props.mfmAutocomplete === true ? null : props.mfmAutocomplete);
+ }
+});
+
+onUnmounted(() => {
+ if (autocomplete) {
+ autocomplete.detach();
+ }
});
</script>
@@ -194,4 +223,12 @@ onMounted(() => {
.save {
margin: 8px 0 0 0;
}
+
+.mfmPreview {
+ padding: 12px;
+ border-radius: var(--radius);
+ box-sizing: border-box;
+ min-height: 130px;
+ pointer-events: none;
+}
</style>
diff --git a/packages/frontend/src/components/MkTimeline.vue b/packages/frontend/src/components/MkTimeline.vue
index 85096dc583..8bd68c0fd2 100644
--- a/packages/frontend/src/components/MkTimeline.vue
+++ b/packages/frontend/src/components/MkTimeline.vue
@@ -17,7 +17,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { computed, watch, onUnmounted, provide } from 'vue';
+import { computed, watch, onUnmounted, provide, ref } from 'vue';
import { Connection } from 'misskey-js/built/streaming.js';
import MkNotes from '@/components/MkNotes.vue';
import MkPullToRefresh from '@/components/MkPullToRefresh.vue';
@@ -65,8 +65,8 @@ type TimelineQueryType = {
roleId?: string
}
-const prComponent: InstanceType<typeof MkPullToRefresh> = $ref();
-const tlComponent: InstanceType<typeof MkNotes> = $ref();
+const prComponent = ref<InstanceType<typeof MkPullToRefresh>>();
+const tlComponent = ref<InstanceType<typeof MkNotes>>();
let tlNotesCount = 0;
@@ -77,7 +77,7 @@ const prepend = note => {
note._shouldInsertAd_ = true;
}
- tlComponent.pagingComponent?.prepend(note);
+ tlComponent.value.pagingComponent?.prepend(note);
emit('note');
@@ -271,7 +271,7 @@ function reloadTimeline() {
return new Promise<void>((res) => {
tlNotesCount = 0;
- tlComponent.pagingComponent?.reload().then(() => {
+ tlComponent.value.pagingComponent?.reload().then(() => {
res();
});
});
diff --git a/packages/frontend/src/components/MkToast.vue b/packages/frontend/src/components/MkToast.vue
index 3b26b50a0b..82cd236193 100644
--- a/packages/frontend/src/components/MkToast.vue
+++ b/packages/frontend/src/components/MkToast.vue
@@ -22,7 +22,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { onMounted } from 'vue';
+import { onMounted, ref } from 'vue';
import * as os from '@/os.js';
import { defaultStore } from '@/store.js';
@@ -35,11 +35,11 @@ const emit = defineEmits<{
}>();
const zIndex = os.claimZIndex('high');
-let showing = $ref(true);
+const showing = ref(true);
onMounted(() => {
window.setTimeout(() => {
- showing = false;
+ showing.value = false;
}, 4000);
});
</script>
diff --git a/packages/frontend/src/components/MkTokenGenerateWindow.vue b/packages/frontend/src/components/MkTokenGenerateWindow.vue
index 8958accc4a..f5fa86a908 100644
--- a/packages/frontend/src/components/MkTokenGenerateWindow.vue
+++ b/packages/frontend/src/components/MkTokenGenerateWindow.vue
@@ -41,7 +41,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { } from 'vue';
+import { shallowRef, ref } from 'vue';
import * as Misskey from 'misskey-js';
import MkInput from './MkInput.vue';
import MkSwitch from './MkSwitch.vue';
@@ -67,37 +67,37 @@ const emit = defineEmits<{
(ev: 'done', result: { name: string | null, permissions: string[] }): void;
}>();
-const dialog = $shallowRef<InstanceType<typeof MkModalWindow>>();
-let name = $ref(props.initialName);
-let permissions = $ref({});
+const dialog = shallowRef<InstanceType<typeof MkModalWindow>>();
+const name = ref(props.initialName);
+const permissions = ref({});
if (props.initialPermissions) {
for (const kind of props.initialPermissions) {
- permissions[kind] = true;
+ permissions.value[kind] = true;
}
} else {
for (const kind of Misskey.permissions) {
- permissions[kind] = false;
+ permissions.value[kind] = false;
}
}
function ok(): void {
emit('done', {
- name: name,
- permissions: Object.keys(permissions).filter(p => permissions[p]),
+ name: name.value,
+ permissions: Object.keys(permissions.value).filter(p => permissions.value[p]),
});
- dialog.close();
+ dialog.value.close();
}
function disableAll(): void {
- for (const p in permissions) {
- permissions[p] = false;
+ for (const p in permissions.value) {
+ permissions.value[p] = false;
}
}
function enableAll(): void {
- for (const p in permissions) {
- permissions[p] = true;
+ for (const p in permissions.value) {
+ permissions.value[p] = true;
}
}
</script>
diff --git a/packages/frontend/src/components/MkTutorialDialog.Timeline.vue b/packages/frontend/src/components/MkTutorialDialog.Timeline.vue
index 421c0a8af8..c2384423fd 100644
--- a/packages/frontend/src/components/MkTutorialDialog.Timeline.vue
+++ b/packages/frontend/src/components/MkTutorialDialog.Timeline.vue
@@ -19,7 +19,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<div :class="$style.divider"></div>
<I18n :src="i18n.ts._initialTutorial._timeline.description3" tag="div" style="padding: 0 16px;">
<template #link>
- <a href="https://misskey-hub.net/docs/features/timeline.html" target="_blank" class="_link">{{ i18n.ts.help }}</a>
+ <a href="https://misskey-hub.net/docs/for-users/features/timeline/" target="_blank" class="_link">{{ i18n.ts.help }}</a>
</template>
</I18n>
diff --git a/packages/frontend/src/components/MkTutorialDialog.vue b/packages/frontend/src/components/MkTutorialDialog.vue
index 5db2cc100a..a734f93ec9 100644
--- a/packages/frontend/src/components/MkTutorialDialog.vue
+++ b/packages/frontend/src/components/MkTutorialDialog.vue
@@ -130,7 +130,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<div style="font-size: 120%;">{{ i18n.ts._initialTutorial._done.title }}</div>
<I18n :src="i18n.ts._initialTutorial._done.description" tag="div" style="padding: 0 16px;">
<template #link>
- <a href="https://misskey-hub.net/help.html" target="_blank" class="_link">{{ i18n.ts.help }}</a>
+ <a href="https://misskey-hub.net/docs/for-users/" target="_blank" class="_link">{{ i18n.ts.help }}</a>
</template>
</I18n>
<div>{{ i18n.t('_initialAccountSetting.haveFun', { name: instance.name ?? host }) }}</div>
diff --git a/packages/frontend/src/components/MkUrlPreview.vue b/packages/frontend/src/components/MkUrlPreview.vue
index 78c62e1250..486aaa0bbd 100644
--- a/packages/frontend/src/components/MkUrlPreview.vue
+++ b/packages/frontend/src/components/MkUrlPreview.vue
@@ -31,7 +31,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<iframe
ref="tweet"
allow="fullscreen;web-share"
- sandbox="allow-popups allow-scripts allow-same-origin"
+ sandbox="allow-popups allow-popups-to-escape-sandbox allow-scripts allow-same-origin"
scrolling="no"
:style="{ position: 'relative', width: '100%', height: `${tweetHeight}px`, border: 0 }"
:src="`https://platform.twitter.com/embed/index.html?embedId=${embedId}&amp;hideCard=false&amp;hideThread=false&amp;lang=en&amp;theme=${defaultStore.state.darkMode ? 'dark' : 'light'}&amp;id=${tweetId}`"
@@ -45,7 +45,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<div v-else>
<component :is="self ? 'MkA' : 'a'" :class="[$style.link, { [$style.compact]: compact }]" :[attr]="self ? url.substring(local.length) : url" rel="nofollow noopener" :target="target" :title="url">
- <div v-if="thumbnail" :class="$style.thumbnail" :style="defaultStore.state.enableDataSaverMode ? '' : `background-image: url('${thumbnail}')`">
+ <div v-if="thumbnail && !sensitive" :class="$style.thumbnail" :style="defaultStore.state.dataSaver.urlPreview ? '' : `background-image: url('${thumbnail}')`">
</div>
<article :class="$style.body">
<header :class="$style.header">
@@ -83,7 +83,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { defineAsyncComponent, onUnmounted } from 'vue';
+import { defineAsyncComponent, onUnmounted, ref } from 'vue';
import type { summaly } from 'summaly';
import { url as local } from '@/config.js';
import { i18n } from '@/i18n.js';
@@ -107,35 +107,36 @@ const props = withDefaults(defineProps<{
});
const MOBILE_THRESHOLD = 500;
-const isMobile = $ref(deviceKind === 'smartphone' || window.innerWidth <= MOBILE_THRESHOLD);
+const isMobile = ref(deviceKind === 'smartphone' || window.innerWidth <= MOBILE_THRESHOLD);
const self = props.url.startsWith(local);
const attr = self ? 'to' : 'href';
const target = self ? null : '_blank';
-let fetching = $ref(true);
-let title = $ref<string | null>(null);
-let description = $ref<string | null>(null);
-let thumbnail = $ref<string | null>(null);
-let icon = $ref<string | null>(null);
-let sitename = $ref<string | null>(null);
-let player = $ref({
+const fetching = ref(true);
+const title = ref<string | null>(null);
+const description = ref<string | null>(null);
+const thumbnail = ref<string | null>(null);
+const icon = ref<string | null>(null);
+const sitename = ref<string | null>(null);
+const sensitive = ref<boolean>(false);
+const player = ref({
url: null,
width: null,
height: null,
} as SummalyResult['player']);
-let playerEnabled = $ref(false);
-let tweetId = $ref<string | null>(null);
-let tweetExpanded = $ref(props.detail);
+const playerEnabled = ref(false);
+const tweetId = ref<string | null>(null);
+const tweetExpanded = ref(props.detail);
const embedId = `embed${Math.random().toString().replace(/\D/, '')}`;
-let tweetHeight = $ref(150);
-let unknownUrl = $ref(false);
+const tweetHeight = ref(150);
+const unknownUrl = ref(false);
const requestUrl = new URL(props.url);
if (!['http:', 'https:'].includes(requestUrl.protocol)) throw new Error('invalid url');
if (requestUrl.hostname === 'twitter.com' || requestUrl.hostname === 'mobile.twitter.com' || requestUrl.hostname === 'x.com' || requestUrl.hostname === 'mobile.x.com') {
const m = requestUrl.pathname.match(/^\/.+\/status(?:es)?\/(\d+)/);
- if (m) tweetId = m[1];
+ if (m) tweetId.value = m[1];
}
if (requestUrl.hostname === 'music.youtube.com' && requestUrl.pathname.match('^/(?:watch|channel)')) {
@@ -147,8 +148,8 @@ requestUrl.hash = '';
window.fetch(`/url?url=${encodeURIComponent(requestUrl.href)}&lang=${versatileLang}`)
.then(res => {
if (!res.ok) {
- fetching = false;
- unknownUrl = true;
+ fetching.value = false;
+ unknownUrl.value = true;
return;
}
@@ -156,20 +157,21 @@ window.fetch(`/url?url=${encodeURIComponent(requestUrl.href)}&lang=${versatileLa
})
.then((info: SummalyResult) => {
if (info.url == null) {
- fetching = false;
- unknownUrl = true;
+ fetching.value = false;
+ unknownUrl.value = true;
return;
}
- fetching = false;
- unknownUrl = false;
+ fetching.value = false;
+ unknownUrl.value = false;
- title = info.title;
- description = info.description;
- thumbnail = info.thumbnail;
- icon = info.icon;
- sitename = info.sitename;
- player = info.player;
+ title.value = info.title;
+ description.value = info.description;
+ thumbnail.value = info.thumbnail;
+ icon.value = info.icon;
+ sitename.value = info.sitename;
+ player.value = info.player;
+ sensitive.value = info.sensitive ?? false;
});
function adjustTweetHeight(message: any) {
@@ -178,7 +180,7 @@ function adjustTweetHeight(message: any) {
if (embed?.method !== 'twttr.private.resize') return;
if (embed?.id !== embedId) return;
const height = embed?.params[0]?.height;
- if (height) tweetHeight = height;
+ if (height) tweetHeight.value = height;
}
const openPlayer = (): void => {
diff --git a/packages/frontend/src/components/MkUrlPreviewPopup.vue b/packages/frontend/src/components/MkUrlPreviewPopup.vue
index 0ab012dfb7..81c383540c 100644
--- a/packages/frontend/src/components/MkUrlPreviewPopup.vue
+++ b/packages/frontend/src/components/MkUrlPreviewPopup.vue
@@ -12,7 +12,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { onMounted } from 'vue';
+import { onMounted, ref } from 'vue';
import MkUrlPreview from '@/components/MkUrlPreview.vue';
import * as os from '@/os.js';
import { defaultStore } from '@/store.js';
@@ -28,16 +28,16 @@ const emit = defineEmits<{
}>();
const zIndex = os.claimZIndex('middle');
-let top = $ref(0);
-let left = $ref(0);
+const top = ref(0);
+const left = ref(0);
onMounted(() => {
const rect = props.source.getBoundingClientRect();
const x = Math.max((rect.left + (props.source.offsetWidth / 2)) - (300 / 2), 6) + window.pageXOffset;
const y = rect.top + props.source.offsetHeight + window.pageYOffset;
- top = y;
- left = x;
+ top.value = y;
+ left.value = x;
});
</script>
diff --git a/packages/frontend/src/components/MkUserAnnouncementEditDialog.vue b/packages/frontend/src/components/MkUserAnnouncementEditDialog.vue
index 42ccb621b6..e1237659c2 100644
--- a/packages/frontend/src/components/MkUserAnnouncementEditDialog.vue
+++ b/packages/frontend/src/components/MkUserAnnouncementEditDialog.vue
@@ -50,7 +50,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { } from 'vue';
+import { ref } from 'vue';
import * as Misskey from 'misskey-js';
import MkModalWindow from '@/components/MkModalWindow.vue';
import MkButton from '@/components/MkButton.vue';
@@ -66,12 +66,12 @@ const props = defineProps<{
announcement?: any,
}>();
-let dialog = $ref(null);
-let title: string = $ref(props.announcement ? props.announcement.title : '');
-let text: string = $ref(props.announcement ? props.announcement.text : '');
-let icon: string = $ref(props.announcement ? props.announcement.icon : 'info');
-let display: string = $ref(props.announcement ? props.announcement.display : 'dialog');
-let needConfirmationToRead = $ref(props.announcement ? props.announcement.needConfirmationToRead : false);
+const dialog = ref(null);
+const title = ref<string>(props.announcement ? props.announcement.title : '');
+const text = ref<string>(props.announcement ? props.announcement.text : '');
+const icon = ref<string>(props.announcement ? props.announcement.icon : 'info');
+const display = ref<string>(props.announcement ? props.announcement.display : 'dialog');
+const needConfirmationToRead = ref(props.announcement ? props.announcement.needConfirmationToRead : false);
const emit = defineEmits<{
(ev: 'done', v: { deleted?: boolean; updated?: any; created?: any }): void,
@@ -80,12 +80,12 @@ const emit = defineEmits<{
async function done() {
const params = {
- title: title,
- text: text,
- icon: icon,
+ title: title.value,
+ text: text.value,
+ icon: icon.value,
imageUrl: null,
- display: display,
- needConfirmationToRead: needConfirmationToRead,
+ display: display.value,
+ needConfirmationToRead: needConfirmationToRead.value,
userId: props.user.id,
};
@@ -102,7 +102,7 @@ async function done() {
},
});
- dialog.close();
+ dialog.value.close();
} else {
const created = await os.apiWithDialog('admin/announcements/create', params);
@@ -110,14 +110,14 @@ async function done() {
created: created,
});
- dialog.close();
+ dialog.value.close();
}
}
async function del() {
const { canceled } = await os.confirm({
type: 'warning',
- text: i18n.t('removeAreYouSure', { x: title }),
+ text: i18n.t('removeAreYouSure', { x: title.value }),
});
if (canceled) return;
@@ -127,7 +127,7 @@ async function del() {
emit('done', {
deleted: true,
});
- dialog.close();
+ dialog.value.close();
});
}
</script>
diff --git a/packages/frontend/src/components/MkUserCardMini.vue b/packages/frontend/src/components/MkUserCardMini.vue
index 978c5005c8..b9c7377972 100644
--- a/packages/frontend/src/components/MkUserCardMini.vue
+++ b/packages/frontend/src/components/MkUserCardMini.vue
@@ -16,7 +16,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import * as Misskey from 'misskey-js';
-import { onMounted } from 'vue';
+import { onMounted, ref } from 'vue';
import MkMiniChart from '@/components/MkMiniChart.vue';
import * as os from '@/os.js';
import { acct } from '@/filters/user.js';
@@ -28,14 +28,14 @@ const props = withDefaults(defineProps<{
withChart: true,
});
-let chartValues = $ref<number[] | null>(null);
+const chartValues = ref<number[] | null>(null);
onMounted(() => {
if (props.withChart) {
os.apiGet('charts/user/notes', { userId: props.user.id, limit: 16 + 1, span: 'day' }).then(res => {
// 今日のぶんの値はまだ途中の値であり、それも含めると大抵の場合前日よりも下降しているようなグラフになってしまうため今日は弾く
res.inc.splice(0, 1);
- chartValues = res.inc;
+ chartValues.value = res.inc;
});
}
});
diff --git a/packages/frontend/src/components/MkUserInfo.vue b/packages/frontend/src/components/MkUserInfo.vue
index 322ffee38e..4e326911d8 100644
--- a/packages/frontend/src/components/MkUserInfo.vue
+++ b/packages/frontend/src/components/MkUserInfo.vue
@@ -22,10 +22,10 @@ SPDX-License-Identifier: AGPL-3.0-only
<div :class="$style.statusItem">
<p :class="$style.statusItemLabel">{{ i18n.ts.notes }}</p><span :class="$style.statusItemValue">{{ number(user.notesCount) }}</span>
</div>
- <div v-if="isFfVisibleForMe(user)" :class="$style.statusItem">
+ <div v-if="isFollowingVisibleForMe(user)" :class="$style.statusItem">
<p :class="$style.statusItemLabel">{{ i18n.ts.following }}</p><span :class="$style.statusItemValue">{{ number(user.followingCount) }}</span>
</div>
- <div v-if="isFfVisibleForMe(user)" :class="$style.statusItem">
+ <div v-if="isFollowersVisibleForMe(user)" :class="$style.statusItem">
<p :class="$style.statusItemLabel">{{ i18n.ts.followers }}</p><span :class="$style.statusItemValue">{{ number(user.followersCount) }}</span>
</div>
</div>
@@ -40,7 +40,7 @@ import number from '@/filters/number.js';
import { userPage } from '@/filters/user.js';
import { i18n } from '@/i18n.js';
import { $i } from '@/account.js';
-import { isFfVisibleForMe } from '@/scripts/isFfVisibleForMe.js';
+import { isFollowingVisibleForMe, isFollowersVisibleForMe } from '@/scripts/isFfVisibleForMe.js';
defineProps<{
user: Misskey.entities.UserDetailed;
diff --git a/packages/frontend/src/components/MkUserOnlineIndicator.vue b/packages/frontend/src/components/MkUserOnlineIndicator.vue
index c6e1218c0f..76470cba88 100644
--- a/packages/frontend/src/components/MkUserOnlineIndicator.vue
+++ b/packages/frontend/src/components/MkUserOnlineIndicator.vue
@@ -16,7 +16,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { } from 'vue';
+import { computed } from 'vue';
import * as Misskey from 'misskey-js';
import { i18n } from '@/i18n.js';
@@ -24,7 +24,7 @@ const props = defineProps<{
user: Misskey.entities.User;
}>();
-const text = $computed(() => {
+const text = computed(() => {
switch (props.user.onlineStatus) {
case 'online': return i18n.ts.online;
case 'active': return i18n.ts.active;
diff --git a/packages/frontend/src/components/MkUserPopup.vue b/packages/frontend/src/components/MkUserPopup.vue
index d958b325e5..ec2c48b1cf 100644
--- a/packages/frontend/src/components/MkUserPopup.vue
+++ b/packages/frontend/src/components/MkUserPopup.vue
@@ -47,11 +47,11 @@ SPDX-License-Identifier: AGPL-3.0-only
<div :class="$style.statusItemLabel">{{ i18n.ts.notes }}</div>
<div>{{ number(user.notesCount) }}</div>
</div>
- <div v-if="isFfVisibleForMe(user)" :class="$style.statusItem">
+ <div v-if="isFollowingVisibleForMe(user)" :class="$style.statusItem">
<div :class="$style.statusItemLabel">{{ i18n.ts.following }}</div>
<div>{{ number(user.followingCount) }}</div>
</div>
- <div v-if="isFfVisibleForMe(user)" :class="$style.statusItem">
+ <div v-if="isFollowersVisibleForMe(user)" :class="$style.statusItem">
<div :class="$style.statusItemLabel">{{ i18n.ts.followers }}</div>
<div>{{ number(user.followersCount) }}</div>
</div>
@@ -67,7 +67,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { onMounted } from 'vue';
+import { onMounted, ref } from 'vue';
import * as Misskey from 'misskey-js';
import MkFollowButton from '@/components/MkFollowButton.vue';
import { userPage } from '@/filters/user.js';
@@ -77,7 +77,7 @@ import number from '@/filters/number.js';
import { i18n } from '@/i18n.js';
import { defaultStore } from '@/store.js';
import { $i } from '@/account.js';
-import { isFfVisibleForMe } from '@/scripts/isFfVisibleForMe.js';
+import { isFollowingVisibleForMe, isFollowersVisibleForMe } from '@/scripts/isFfVisibleForMe.js';
const props = defineProps<{
showing: boolean;
@@ -92,18 +92,18 @@ const emit = defineEmits<{
}>();
const zIndex = os.claimZIndex('middle');
-let user = $ref<Misskey.entities.UserDetailed | null>(null);
-let top = $ref(0);
-let left = $ref(0);
+const user = ref<Misskey.entities.UserDetailed | null>(null);
+const top = ref(0);
+const left = ref(0);
function showMenu(ev: MouseEvent) {
- const { menu, cleanup } = getUserMenu(user);
+ const { menu, cleanup } = getUserMenu(user.value);
os.popupMenu(menu, ev.currentTarget ?? ev.target).finally(cleanup);
}
onMounted(() => {
if (typeof props.q === 'object') {
- user = props.q;
+ user.value = props.q;
} else {
const query = props.q.startsWith('@') ?
Misskey.acct.parse(props.q.substring(1)) :
@@ -111,7 +111,7 @@ onMounted(() => {
os.api('users/show', query).then(res => {
if (!props.showing) return;
- user = res;
+ user.value = res;
});
}
@@ -119,8 +119,8 @@ onMounted(() => {
const x = ((rect.left + (props.source.offsetWidth / 2)) - (300 / 2)) + window.pageXOffset;
const y = rect.top + props.source.offsetHeight + window.pageYOffset;
- top = y;
- left = x;
+ top.value = y;
+ left.value = x;
});
</script>
diff --git a/packages/frontend/src/components/MkUserSelectDialog.vue b/packages/frontend/src/components/MkUserSelectDialog.vue
index ac38c4b62f..9d41147bd2 100644
--- a/packages/frontend/src/components/MkUserSelectDialog.vue
+++ b/packages/frontend/src/components/MkUserSelectDialog.vue
@@ -57,7 +57,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { onMounted } from 'vue';
+import { onMounted, ref } from 'vue';
import * as Misskey from 'misskey-js';
import MkInput from '@/components/MkInput.vue';
import FormSplit from '@/components/form/split.vue';
@@ -78,43 +78,43 @@ const props = defineProps<{
includeSelf?: boolean;
}>();
-let username = $ref('');
-let host = $ref('');
-let users: Misskey.entities.UserDetailed[] = $ref([]);
-let recentUsers: Misskey.entities.UserDetailed[] = $ref([]);
-let selected: Misskey.entities.UserDetailed | null = $ref(null);
-let dialogEl = $ref();
+const username = ref('');
+const host = ref('');
+const users = ref<Misskey.entities.UserDetailed[]>([]);
+const recentUsers = ref<Misskey.entities.UserDetailed[]>([]);
+const selected = ref<Misskey.entities.UserDetailed | null>(null);
+const dialogEl = ref();
const search = () => {
- if (username === '' && host === '') {
- users = [];
+ if (username.value === '' && host.value === '') {
+ users.value = [];
return;
}
os.api('users/search-by-username-and-host', {
- username: username,
- host: host,
+ username: username.value,
+ host: host.value,
limit: 10,
detail: false,
}).then(_users => {
- users = _users;
+ users.value = _users;
});
};
const ok = () => {
- if (selected == null) return;
- emit('ok', selected);
- dialogEl.close();
+ if (selected.value == null) return;
+ emit('ok', selected.value);
+ dialogEl.value.close();
// 最近使ったユーザー更新
let recents = defaultStore.state.recentlyUsedUsers;
- recents = recents.filter(x => x !== selected.id);
- recents.unshift(selected.id);
+ recents = recents.filter(x => x !== selected.value.id);
+ recents.unshift(selected.value.id);
defaultStore.set('recentlyUsedUsers', recents.splice(0, 16));
};
const cancel = () => {
emit('cancel');
- dialogEl.close();
+ dialogEl.value.close();
};
onMounted(() => {
@@ -122,9 +122,9 @@ onMounted(() => {
userIds: defaultStore.state.recentlyUsedUsers,
}).then(users => {
if (props.includeSelf && users.find(x => $i ? x.id === $i.id : true) == null) {
- recentUsers = [$i, ...users];
+ recentUsers.value = [$i, ...users];
} else {
- recentUsers = users;
+ recentUsers.value = users;
}
});
});
diff --git a/packages/frontend/src/components/MkUserSetupDialog.Follow.vue b/packages/frontend/src/components/MkUserSetupDialog.Follow.vue
index 4ecca7334c..5f3f5b81dd 100644
--- a/packages/frontend/src/components/MkUserSetupDialog.Follow.vue
+++ b/packages/frontend/src/components/MkUserSetupDialog.Follow.vue
@@ -34,15 +34,9 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { computed, ref, watch } from 'vue';
-import { instance } from '@/instance.js';
import { i18n } from '@/i18n.js';
-import MkButton from '@/components/MkButton.vue';
import MkFolder from '@/components/MkFolder.vue';
import XUser from '@/components/MkUserSetupDialog.User.vue';
-import MkInfo from '@/components/MkInfo.vue';
-import * as os from '@/os.js';
-import { $i } from '@/account.js';
import MkPagination from '@/components/MkPagination.vue';
const pinnedUsers = { endpoint: 'pinned-users', noPaging: true };
diff --git a/packages/frontend/src/components/MkUserSetupDialog.Privacy.vue b/packages/frontend/src/components/MkUserSetupDialog.Privacy.vue
index 7401dbddb1..664c4da203 100644
--- a/packages/frontend/src/components/MkUserSetupDialog.Privacy.vue
+++ b/packages/frontend/src/components/MkUserSetupDialog.Privacy.vue
@@ -36,18 +36,16 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { computed, ref, watch } from 'vue';
-import { instance } from '@/instance.js';
+import { ref, watch } from 'vue';
import { i18n } from '@/i18n.js';
import MkSwitch from '@/components/MkSwitch.vue';
import MkInfo from '@/components/MkInfo.vue';
import MkFolder from '@/components/MkFolder.vue';
import * as os from '@/os.js';
-import { $i } from '@/account.js';
-let isLocked = ref(false);
-let hideOnlineStatus = ref(false);
-let noCrawle = ref(false);
+const isLocked = ref(false);
+const hideOnlineStatus = ref(false);
+const noCrawle = ref(false);
watch([isLocked, hideOnlineStatus, noCrawle], () => {
os.api('i/update', {
diff --git a/packages/frontend/src/components/MkUserSetupDialog.Profile.vue b/packages/frontend/src/components/MkUserSetupDialog.Profile.vue
index 8de9bbdbb1..37aa677b44 100644
--- a/packages/frontend/src/components/MkUserSetupDialog.Profile.vue
+++ b/packages/frontend/src/components/MkUserSetupDialog.Profile.vue
@@ -30,8 +30,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { computed, ref, watch } from 'vue';
-import { instance } from '@/instance.js';
+import { ref, watch } from 'vue';
import { i18n } from '@/i18n.js';
import MkButton from '@/components/MkButton.vue';
import MkInput from '@/components/MkInput.vue';
diff --git a/packages/frontend/src/components/MkUserSetupDialog.User.vue b/packages/frontend/src/components/MkUserSetupDialog.User.vue
index 01a943b7a0..621995cc5b 100644
--- a/packages/frontend/src/components/MkUserSetupDialog.User.vue
+++ b/packages/frontend/src/components/MkUserSetupDialog.User.vue
@@ -29,7 +29,6 @@ import * as Misskey from 'misskey-js';
import { ref } from 'vue';
import MkButton from '@/components/MkButton.vue';
import { i18n } from '@/i18n.js';
-import { $i } from '@/account.js';
import * as os from '@/os.js';
const props = defineProps<{
diff --git a/packages/frontend/src/components/MkVisibilityPicker.vue b/packages/frontend/src/components/MkVisibilityPicker.vue
index 325829a8a8..61edc345a9 100644
--- a/packages/frontend/src/components/MkVisibilityPicker.vue
+++ b/packages/frontend/src/components/MkVisibilityPicker.vue
@@ -42,12 +42,12 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { nextTick } from 'vue';
+import { nextTick, shallowRef, ref } from 'vue';
import * as Misskey from 'misskey-js';
import MkModal from '@/components/MkModal.vue';
import { i18n } from '@/i18n.js';
-const modal = $shallowRef<InstanceType<typeof MkModal>>();
+const modal = shallowRef<InstanceType<typeof MkModal>>();
const props = withDefaults(defineProps<{
currentVisibility: typeof Misskey.noteVisibilities[number];
@@ -62,13 +62,13 @@ const emit = defineEmits<{
(ev: 'closed'): void;
}>();
-let v = $ref(props.currentVisibility);
+const v = ref(props.currentVisibility);
function choose(visibility: typeof Misskey.noteVisibilities[number]): void {
- v = visibility;
+ v.value = visibility;
emit('changeVisibility', visibility);
nextTick(() => {
- if (modal) modal.close();
+ if (modal.value) modal.value.close();
});
}
</script>
diff --git a/packages/frontend/src/components/MkVisitorDashboard.ActiveUsersChart.vue b/packages/frontend/src/components/MkVisitorDashboard.ActiveUsersChart.vue
index 26de7dee52..746ed3e0de 100644
--- a/packages/frontend/src/components/MkVisitorDashboard.ActiveUsersChart.vue
+++ b/packages/frontend/src/components/MkVisitorDashboard.ActiveUsersChart.vue
@@ -13,7 +13,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { onMounted } from 'vue';
+import { onMounted, shallowRef, ref } from 'vue';
import { Chart } from 'chart.js';
import gradient from 'chartjs-plugin-gradient';
import tinycolor from 'tinycolor2';
@@ -25,11 +25,11 @@ import { initChart } from '@/scripts/init-chart.js';
initChart();
-const chartEl = $shallowRef<HTMLCanvasElement>(null);
+const chartEl = shallowRef<HTMLCanvasElement>(null);
const now = new Date();
let chartInstance: Chart = null;
const chartLimit = 30;
-let fetching = $ref(true);
+const fetching = ref(true);
const { handler: externalTooltipHandler } = useChartTooltip();
@@ -65,7 +65,7 @@ async function renderChart() {
const max = Math.max(...raw.read);
- chartInstance = new Chart(chartEl, {
+ chartInstance = new Chart(chartEl.value, {
type: 'bar',
data: {
datasets: [{
@@ -147,7 +147,7 @@ async function renderChart() {
plugins: [chartVLine(vLineColor)],
});
- fetching = false;
+ fetching.value = false;
}
onMounted(async () => {
diff --git a/packages/frontend/src/components/MkVisitorDashboard.vue b/packages/frontend/src/components/MkVisitorDashboard.vue
index fe76ded7b4..862a38bd54 100644
--- a/packages/frontend/src/components/MkVisitorDashboard.vue
+++ b/packages/frontend/src/components/MkVisitorDashboard.vue
@@ -54,9 +54,8 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { } from 'vue';
+import { ref } from 'vue';
import * as Misskey from 'misskey-js';
-import XTimeline from './welcome.timeline.vue';
import XSigninDialog from '@/components/MkSigninDialog.vue';
import XSignupDialog from '@/components/MkSignupDialog.vue';
import MkButton from '@/components/MkButton.vue';
@@ -66,20 +65,18 @@ import { instanceName } from '@/config.js';
import * as os from '@/os.js';
import { i18n } from '@/i18n.js';
import { instance } from '@/instance.js';
-import number from '@/filters/number.js';
import MkNumber from '@/components/MkNumber.vue';
import XActiveUsersChart from '@/components/MkVisitorDashboard.ActiveUsersChart.vue';
-let meta = $ref<Misskey.entities.Instance>();
-let stats = $ref(null);
+const meta = ref<Misskey.entities.MetaResponse | null>(null);
+const stats = ref<Misskey.entities.StatsResponse | null>(null);
os.api('meta', { detail: true }).then(_meta => {
- meta = _meta;
+ meta.value = _meta;
});
-os.api('stats', {
-}).then((res) => {
- stats = res;
+os.api('stats', {}).then((res) => {
+ stats.value = res;
});
function signin() {
@@ -107,35 +104,35 @@ function showMenu(ev) {
action: () => {
os.pageWindow('/about-sharkey');
},
- }, null, (instance.impressumUrl) ? {
+ }, { type: 'divider' }, (instance.impressumUrl) ? {
text: i18n.ts.impressum,
icon: 'ph-newspaper-clipping ph-bold ph-lg',
action: () => {
- window.open(instance.impressumUrl, '_blank');
+ window.open(instance.impressumUrl, '_blank', 'noopener');
},
} : undefined, (instance.tosUrl) ? {
text: i18n.ts.termsOfService,
icon: 'ph-notebook ph-bold ph-lg',
action: () => {
- window.open(instance.tosUrl, '_blank');
+ window.open(instance.tosUrl, '_blank', 'noopener');
},
} : undefined, (instance.privacyPolicyUrl) ? {
text: i18n.ts.privacyPolicy,
icon: 'ph-shield ph-bold ph-lg',
action: () => {
- window.open(instance.privacyPolicyUrl, '_blank');
+ window.open(instance.privacyPolicyUrl, '_blank', 'noopener');
},
- } : undefined, (!instance.impressumUrl && !instance.tosUrl && !instance.privacyPolicyUrl) ? undefined : null, {
+ } : undefined, (!instance.impressumUrl && !instance.tosUrl && !instance.privacyPolicyUrl) ? undefined : { type: 'divider' }, {
text: i18n.ts.help,
icon: 'ph-question ph-bold ph-lg',
action: () => {
- window.open('https://misskey-hub.net/help.md', '_blank');
+ window.open('https://misskey-hub.net/docs/for-users/', '_blank', 'noopener');
},
}], ev.currentTarget ?? ev.target);
}
function exploreOtherServers() {
- window.open('https://joinsharkey.org/#findaninstance', '_blank');
+ window.open('https://joinsharkey.org/#findaninstance', '_blank', 'noopener');
}
</script>
diff --git a/packages/frontend/src/components/MkWindow.vue b/packages/frontend/src/components/MkWindow.vue
index d6c3e3f81d..e5b8bd9b15 100644
--- a/packages/frontend/src/components/MkWindow.vue
+++ b/packages/frontend/src/components/MkWindow.vue
@@ -53,10 +53,10 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { onBeforeUnmount, onMounted, provide } from 'vue';
+import { onBeforeUnmount, onMounted, provide, shallowRef, ref } from 'vue';
import contains from '@/scripts/contains.js';
import * as os from '@/os.js';
-import { MenuItem } from '@/types/menu';
+import { MenuItem } from '@/types/menu.js';
import { i18n } from '@/i18n.js';
import { defaultStore } from '@/store.js';
@@ -107,18 +107,18 @@ const emit = defineEmits<{
provide('inWindow', true);
-let rootEl = $shallowRef<HTMLElement | null>();
-let showing = $ref(true);
+const rootEl = shallowRef<HTMLElement | null>();
+const showing = ref(true);
let beforeClickedAt = 0;
-let maximized = $ref(false);
-let minimized = $ref(false);
+const maximized = ref(false);
+const minimized = ref(false);
let unResizedTop = '';
let unResizedLeft = '';
let unResizedWidth = '';
let unResizedHeight = '';
function close() {
- showing = false;
+ showing.value = false;
}
function onKeydown(evt) {
@@ -137,46 +137,46 @@ function onContextmenu(ev: MouseEvent) {
// 最前面へ移動
function top() {
- if (rootEl) {
- rootEl.style.zIndex = os.claimZIndex(props.front ? 'middle' : 'low');
+ if (rootEl.value) {
+ rootEl.value.style.zIndex = os.claimZIndex(props.front ? 'middle' : 'low');
}
}
function maximize() {
- maximized = true;
- unResizedTop = rootEl.style.top;
- unResizedLeft = rootEl.style.left;
- unResizedWidth = rootEl.style.width;
- unResizedHeight = rootEl.style.height;
- rootEl.style.top = '0';
- rootEl.style.left = '0';
- rootEl.style.width = '100%';
- rootEl.style.height = '100%';
+ maximized.value = true;
+ unResizedTop = rootEl.value.style.top;
+ unResizedLeft = rootEl.value.style.left;
+ unResizedWidth = rootEl.value.style.width;
+ unResizedHeight = rootEl.value.style.height;
+ rootEl.value.style.top = '0';
+ rootEl.value.style.left = '0';
+ rootEl.value.style.width = '100%';
+ rootEl.value.style.height = '100%';
}
function unMaximize() {
- maximized = false;
- rootEl.style.top = unResizedTop;
- rootEl.style.left = unResizedLeft;
- rootEl.style.width = unResizedWidth;
- rootEl.style.height = unResizedHeight;
+ maximized.value = false;
+ rootEl.value.style.top = unResizedTop;
+ rootEl.value.style.left = unResizedLeft;
+ rootEl.value.style.width = unResizedWidth;
+ rootEl.value.style.height = unResizedHeight;
}
function minimize() {
- minimized = true;
- unResizedWidth = rootEl.style.width;
- unResizedHeight = rootEl.style.height;
- rootEl.style.width = minWidth + 'px';
- rootEl.style.height = props.mini ? '32px' : '39px';
+ minimized.value = true;
+ unResizedWidth = rootEl.value.style.width;
+ unResizedHeight = rootEl.value.style.height;
+ rootEl.value.style.width = minWidth + 'px';
+ rootEl.value.style.height = props.mini ? '32px' : '39px';
}
function unMinimize() {
- const main = rootEl;
+ const main = rootEl.value;
if (main == null) return;
- minimized = false;
- rootEl.style.width = unResizedWidth;
- rootEl.style.height = unResizedHeight;
+ minimized.value = false;
+ rootEl.value.style.width = unResizedWidth;
+ rootEl.value.style.height = unResizedHeight;
const browserWidth = window.innerWidth;
const browserHeight = window.innerHeight;
const windowWidth = main.offsetWidth;
@@ -192,7 +192,7 @@ function onBodyMousedown() {
}
function onDblClick() {
- if (minimized) {
+ if (minimized.value) {
unMinimize();
} else {
maximize();
@@ -205,7 +205,7 @@ function onHeaderMousedown(evt: MouseEvent) {
let beforeMaximized = false;
- if (maximized) {
+ if (maximized.value) {
beforeMaximized = true;
unMaximize();
}
@@ -219,7 +219,7 @@ function onHeaderMousedown(evt: MouseEvent) {
beforeClickedAt = Date.now();
- const main = rootEl;
+ const main = rootEl.value;
if (main == null) return;
if (!contains(main, document.activeElement)) main.focus();
@@ -251,8 +251,8 @@ function onHeaderMousedown(evt: MouseEvent) {
// 右はみ出し
if (moveLeft + windowWidth > browserWidth) moveLeft = browserWidth - windowWidth;
- rootEl.style.left = moveLeft + 'px';
- rootEl.style.top = moveTop + 'px';
+ rootEl.value.style.left = moveLeft + 'px';
+ rootEl.value.style.top = moveTop + 'px';
}
if (beforeMaximized) {
@@ -270,7 +270,7 @@ function onHeaderMousedown(evt: MouseEvent) {
// 上ハンドル掴み時
function onTopHandleMousedown(evt) {
- const main = rootEl;
+ const main = rootEl.value;
// どういうわけかnullになることがある
if (main == null) return;
@@ -298,7 +298,7 @@ function onTopHandleMousedown(evt) {
// 右ハンドル掴み時
function onRightHandleMousedown(evt) {
- const main = rootEl;
+ const main = rootEl.value;
if (main == null) return;
const base = evt.clientX;
@@ -323,7 +323,7 @@ function onRightHandleMousedown(evt) {
// 下ハンドル掴み時
function onBottomHandleMousedown(evt) {
- const main = rootEl;
+ const main = rootEl.value;
if (main == null) return;
const base = evt.clientY;
@@ -348,7 +348,7 @@ function onBottomHandleMousedown(evt) {
// 左ハンドル掴み時
function onLeftHandleMousedown(evt) {
- const main = rootEl;
+ const main = rootEl.value;
if (main == null) return;
const base = evt.clientX;
@@ -400,27 +400,27 @@ function onBottomLeftHandleMousedown(evt) {
// 高さを適用
function applyTransformHeight(height) {
if (height > window.innerHeight) height = window.innerHeight;
- rootEl.style.height = height + 'px';
+ rootEl.value.style.height = height + 'px';
}
// 幅を適用
function applyTransformWidth(width) {
if (width > window.innerWidth) width = window.innerWidth;
- rootEl.style.width = width + 'px';
+ rootEl.value.style.width = width + 'px';
}
// Y座標を適用
function applyTransformTop(top) {
- rootEl.style.top = top + 'px';
+ rootEl.value.style.top = top + 'px';
}
// X座標を適用
function applyTransformLeft(left) {
- rootEl.style.left = left + 'px';
+ rootEl.value.style.left = left + 'px';
}
function onBrowserResize() {
- const main = rootEl;
+ const main = rootEl.value;
if (main == null) return;
const position = main.getBoundingClientRect();
@@ -438,8 +438,8 @@ onMounted(() => {
applyTransformWidth(props.initialWidth);
if (props.initialHeight) applyTransformHeight(props.initialHeight);
- applyTransformTop((window.innerHeight / 2) - (rootEl.offsetHeight / 2));
- applyTransformLeft((window.innerWidth / 2) - (rootEl.offsetWidth / 2));
+ applyTransformTop((window.innerHeight / 2) - (rootEl.value.offsetHeight / 2));
+ applyTransformLeft((window.innerWidth / 2) - (rootEl.value.offsetWidth / 2));
// 他のウィンドウ内のボタンなどを押してこのウィンドウが開かれた場合、親が最前面になろうとするのでそれに隠されないようにする
top();
diff --git a/packages/frontend/src/components/MkYouTubePlayer.vue b/packages/frontend/src/components/MkYouTubePlayer.vue
index 7460515c33..a9b2e8a00d 100644
--- a/packages/frontend/src/components/MkYouTubePlayer.vue
+++ b/packages/frontend/src/components/MkYouTubePlayer.vue
@@ -24,6 +24,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
+import { ref } from 'vue';
import MkWindow from '@/components/MkWindow.vue';
import { versatileLang } from '@/scripts/intl-const.js';
import { defaultStore } from '@/store.js';
@@ -35,22 +36,22 @@ const props = defineProps<{
const requestUrl = new URL(props.url);
if (!['http:', 'https:'].includes(requestUrl.protocol)) throw new Error('invalid url');
-let fetching = $ref(true);
-let title = $ref<string | null>(null);
-let player = $ref({
+const fetching = ref(true);
+const title = ref<string | null>(null);
+const player = ref({
url: null,
width: null,
height: null,
});
const ytFetch = (): void => {
- fetching = true;
+ fetching.value = true;
window.fetch(`/url?url=${encodeURIComponent(requestUrl.href)}&lang=${versatileLang}`).then(res => {
res.json().then(info => {
if (info.url == null) return;
- title = info.title;
- fetching = false;
- player = info.player;
+ title.value = info.title;
+ fetching.value = false;
+ player.value = info.player;
});
});
};
diff --git a/packages/frontend/src/components/SkApprovalUser.vue b/packages/frontend/src/components/SkApprovalUser.vue
index 99dcb717b1..2bf6361ac8 100644
--- a/packages/frontend/src/components/SkApprovalUser.vue
+++ b/packages/frontend/src/components/SkApprovalUser.vue
@@ -27,6 +27,7 @@
</template>
<script lang="ts" setup>
+import { ref } from 'vue';
import * as Misskey from 'misskey-js';
import MkFolder from '@/components/MkFolder.vue';
import MkButton from '@/components/MkButton.vue';
@@ -37,15 +38,15 @@ const props = defineProps<{
user: Misskey.entities.User;
}>();
-let reason = $ref('');
-let email = $ref('');
+let reason = ref('');
+let email = ref('');
function getReason() {
return os.api('admin/show-user', {
userId: props.user.id,
}).then(info => {
- reason = info?.signupReason;
- email = info?.email;
+ reason.value = info?.signupReason;
+ email.value = info?.email;
});
}
diff --git a/packages/frontend/src/components/SkInstanceTicker.vue b/packages/frontend/src/components/SkInstanceTicker.vue
index 4e2856388e..fa7b2a444d 100644
--- a/packages/frontend/src/components/SkInstanceTicker.vue
+++ b/packages/frontend/src/components/SkInstanceTicker.vue
@@ -11,7 +11,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { } from 'vue';
+import { computed } from 'vue';
import { instanceName } from '@/config.js';
import { instance as Instance } from '@/instance.js';
import { getProxiedImageUrlNullable } from '@/scripts/media-proxy.js';
@@ -30,7 +30,7 @@ const instance = props.instance ?? {
themeColor: (document.querySelector('meta[name="theme-color-orig"]') as HTMLMetaElement).content,
};
-const faviconUrl = $computed(() => props.instance ? getProxiedImageUrlNullable(props.instance.faviconUrl, 'preview') : getProxiedImageUrlNullable(Instance.iconUrl, 'preview') ?? getProxiedImageUrlNullable(Instance.faviconUrl, 'preview') ?? '/favicon.ico');
+const faviconUrl = computed(() => props.instance ? getProxiedImageUrlNullable(props.instance.faviconUrl, 'preview') : getProxiedImageUrlNullable(Instance.iconUrl, 'preview') ?? getProxiedImageUrlNullable(Instance.faviconUrl, 'preview') ?? '/favicon.ico');
const themeColor = instance.themeColor ?? '#777777';
diff --git a/packages/frontend/src/components/SkNote.vue b/packages/frontend/src/components/SkNote.vue
index b308f4a07a..cb37861330 100644
--- a/packages/frontend/src/components/SkNote.vue
+++ b/packages/frontend/src/components/SkNote.vue
@@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template>
<div
- v-if="!muted"
+ v-if="!hardMuted && !muted"
v-show="!isDeleted"
ref="el"
v-hotkey="keymap"
@@ -58,9 +58,9 @@ SPDX-License-Identifier: AGPL-3.0-only
<div style="container-type: inline-size;">
<p v-if="appearNote.cw != null" :class="$style.cw">
<Mfm v-if="appearNote.cw != ''" style="margin-right: 8px;" :text="appearNote.cw" :author="appearNote.user" :nyaize="'respect'"/>
- <MkCwButton v-model="showContent" :note="appearNote" style="margin: 4px 0;" v-on:click.stop/>
+ <MkCwButton v-model="showContent" :text="appearNote.text" :renote="appearNote.renote" :files="appearNote.files" :poll="appearNote.poll" style="margin: 4px 0;" @click.stop/>
</p>
- <div v-show="appearNote.cw == null || showContent" :class="[{ [$style.contentCollapsed]: collapsed }]" >
+ <div v-show="appearNote.cw == null || showContent" :class="[{ [$style.contentCollapsed]: collapsed }]">
<div :class="$style.text">
<span v-if="appearNote.isHidden" style="opacity: 0.5">({{ i18n.ts.private }})</span>
<Mfm
@@ -81,31 +81,31 @@ SPDX-License-Identifier: AGPL-3.0-only
<Mfm :text="translation.text" :author="appearNote.user" :nyaize="'respect'" :emojiUrls="appearNote.emojis"/>
</div>
</div>
- <MkButton v-if="!allowAnim && animated" :class="$style.playMFMButton" :small="true" @click="animatedMFM()" v-on:click.stop><i class="ph-play ph-bold ph-lg "></i> {{ i18n.ts._animatedMFM.play }}</MkButton>
- <MkButton v-else-if="!defaultStore.state.animatedMfm && allowAnim && animated" :class="$style.playMFMButton" :small="true" @click="animatedMFM()" v-on:click.stop><i class="ph-stop ph-bold ph-lg "></i> {{ i18n.ts._animatedMFM.stop }}</MkButton>
+ <MkButton v-if="!allowAnim && animated" :class="$style.playMFMButton" :small="true" @click="animatedMFM()" @click.stop><i class="ph-play ph-bold ph-lg "></i> {{ i18n.ts._animatedMFM.play }}</MkButton>
+ <MkButton v-else-if="!defaultStore.state.animatedMfm && allowAnim && animated" :class="$style.playMFMButton" :small="true" @click="animatedMFM()" @click.stop><i class="ph-stop ph-bold ph-lg "></i> {{ i18n.ts._animatedMFM.stop }}</MkButton>
</div>
<div v-if="appearNote.files.length > 0">
- <MkMediaList :mediaList="appearNote.files" v-on:click.stop/>
+ <MkMediaList :mediaList="appearNote.files" @click.stop/>
</div>
- <MkPoll v-if="appearNote.poll" :note="appearNote" :class="$style.poll" v-on:click.stop />
- <MkUrlPreview v-for="url in urls" :key="url" :url="url" :compact="true" :detail="false" :class="$style.urlPreview" v-on:click.stop/>
+ <MkPoll v-if="appearNote.poll" :note="appearNote" :class="$style.poll" @click.stop/>
+ <MkUrlPreview v-for="url in urls" :key="url" :url="url" :compact="true" :detail="false" :class="$style.urlPreview" @click.stop/>
<div v-if="appearNote.renote" :class="$style.quote"><SkNoteSimple :note="appearNote.renote" :class="$style.quoteNote"/></div>
- <button v-if="isLong && collapsed" :class="$style.collapsed" class="_button" v-on:click.stop @click="collapsed = false">
+ <button v-if="isLong && collapsed" :class="$style.collapsed" class="_button" @click.stop @click="collapsed = false">
<span :class="$style.collapsedLabel">{{ i18n.ts.showMore }}</span>
</button>
- <button v-else-if="isLong && !collapsed" :class="$style.showLess" class="_button" v-on:click.stop @click="collapsed = true">
+ <button v-else-if="isLong && !collapsed" :class="$style.showLess" class="_button" @click.stop @click="collapsed = true">
<span :class="$style.showLessLabel">{{ i18n.ts.showLess }}</span>
</button>
</div>
<MkA v-if="appearNote.channel && !inChannel" :class="$style.channel" :to="`/channels/${appearNote.channel.id}`"><i class="ph-television ph-bold ph-lg"></i> {{ appearNote.channel.name }}</MkA>
</div>
- <MkReactionsViewer :note="appearNote" :maxNumber="16" v-on:click.stop @mockUpdateMyReaction="emitUpdReaction">
+ <MkReactionsViewer :note="appearNote" :maxNumber="16" @click.stop @mockUpdateMyReaction="emitUpdReaction">
<template #more>
<div :class="$style.reactionOmitted">{{ i18n.ts.more }}</div>
</template>
</MkReactionsViewer>
<footer :class="$style.footer">
- <button :class="$style.footerButton" class="_button" v-on:click.stop @click="reply()">
+ <button :class="$style.footerButton" class="_button" @click.stop @click="reply()">
<i class="ph-arrow-u-up-left ph-bold ph-lg"></i>
<p v-if="appearNote.repliesCount > 0" :class="$style.footerButtonCount">{{ appearNote.repliesCount }}</p>
</button>
@@ -115,7 +115,7 @@ SPDX-License-Identifier: AGPL-3.0-only
:class="$style.footerButton"
class="_button"
:style="renoted ? 'color: var(--accent) !important;' : ''"
- v-on:click.stop
+ @click.stop
@mousedown="renoted ? undoRenote(appearNote) : boostVisibility()"
>
<i class="ph-rocket-launch ph-bold ph-lg"></i>
@@ -129,19 +129,19 @@ SPDX-License-Identifier: AGPL-3.0-only
ref="quoteButton"
:class="$style.footerButton"
class="_button"
- v-on:click.stop
+ @click.stop
@mousedown="quote()"
>
<i class="ph-quotes ph-bold ph-lg"></i>
</button>
- <button v-if="appearNote.myReaction == null && appearNote.reactionAcceptance !== 'likeOnly'" ref="likeButton" :class="$style.footerButton" class="_button" v-on:click.stop @click="like()">
+ <button v-if="appearNote.myReaction == null && appearNote.reactionAcceptance !== 'likeOnly'" ref="likeButton" :class="$style.footerButton" class="_button" @click.stop @click="like()">
<i class="ph-heart ph-bold ph-lg"></i>
</button>
<button v-if="appearNote.myReaction == null" ref="reactButton" :class="$style.footerButton" class="_button" @mousedown="react()">
<i v-if="appearNote.reactionAcceptance === 'likeOnly'" class="ph-heart ph-bold ph-lg"></i>
<i v-else class="ph-smiley ph-bold ph-lg"></i>
</button>
- <button v-if="appearNote.myReaction != null" ref="reactButton" :class="$style.footerButton" class="_button" v-on:click.stop @click="undoReact(appearNote)">
+ <button v-if="appearNote.myReaction != null" ref="reactButton" :class="$style.footerButton" class="_button" @click.stop @click="undoReact(appearNote)">
<i class="ph-minus ph-bold ph-lg"></i>
</button>
<button v-if="defaultStore.state.showClipButtonInNoteFooter" ref="clipButton" :class="$style.footerButton" class="_button" @mousedown="clip()">
@@ -154,7 +154,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</div>
</article>
</div>
-<div v-else :class="$style.muted" @click="muted = false">
+<div v-else-if="!hardMuted" :class="$style.muted" @click="muted = false">
<I18n :src="i18n.ts.userSaysSomething" tag="small">
<template #name>
<MkA v-user-preview="appearNote.userId" :to="userPage(appearNote.user)">
@@ -163,10 +163,16 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
</I18n>
</div>
+<div v-else>
+ <!--
+ MkDateSeparatedList uses TransitionGroup which requires single element in the child elements
+ so MkNote create empty div instead of no elements
+ -->
+</div>
</template>
<script lang="ts" setup>
-import { computed, inject, onMounted, ref, shallowRef, Ref, defineAsyncComponent, watch, provide } from 'vue';
+import { computed, inject, onMounted, ref, shallowRef, Ref, watch, provide } from 'vue';
import * as mfm from '@sharkey/sfm-js';
import * as Misskey from 'misskey-js';
import SkNoteSub from '@/components/SkNoteSub.vue';
@@ -184,6 +190,7 @@ import { focusPrev, focusNext } from '@/scripts/focus.js';
import { checkWordMute } from '@/scripts/check-word-mute.js';
import { userPage } from '@/filters/user.js';
import * as os from '@/os.js';
+import * as sound from '@/scripts/sound.js';
import { defaultStore, noteViewInterruptors } from '@/store.js';
import { reactionPicker } from '@/scripts/reaction-picker.js';
import { extractUrlFromMfm } from '@/scripts/extract-url-from-mfm.js';
@@ -207,6 +214,7 @@ const props = withDefaults(defineProps<{
note: Misskey.entities.Note;
pinned?: boolean;
mock?: boolean;
+ withHardMute?: boolean;
}>(), {
mock: false,
});
@@ -223,7 +231,7 @@ const router = useRouter();
const inChannel = inject('inChannel', null);
const currentClip = inject<Ref<Misskey.entities.Clip> | null>('currentClip', null);
-let note = $ref(deepClone(props.note));
+const note = ref(deepClone(props.note));
function noteclick(id: string) {
const selection = document.getSelection();
@@ -235,7 +243,7 @@ function noteclick(id: string) {
// plugin
if (noteViewInterruptors.length > 0) {
onMounted(async () => {
- let result: Misskey.entities.Note | null = deepClone(note);
+ let result: Misskey.entities.Note | null = deepClone(note.value);
for (const interruptor of noteViewInterruptors) {
try {
result = await interruptor.handler(result);
@@ -247,15 +255,16 @@ if (noteViewInterruptors.length > 0) {
console.error(err);
}
}
- note = result;
+ note.value = result;
});
}
const isRenote = (
- note.renote != null &&
- note.text == null &&
- note.fileIds.length === 0 &&
- note.poll == null
+ note.value.renote != null &&
+ note.value.text == null &&
+ note.value.cw == null &&
+ note.value.fileIds.length === 0 &&
+ note.value.poll == null
);
const el = shallowRef<HTMLElement>();
@@ -267,27 +276,37 @@ const reactButton = shallowRef<HTMLElement>();
const quoteButton = shallowRef<HTMLElement>();
const clipButton = shallowRef<HTMLElement>();
const likeButton = shallowRef<HTMLElement>();
-let appearNote = $computed(() => isRenote ? note.renote as Misskey.entities.Note : note);
-const renoteUrl = appearNote.renote ? appearNote.renote.url : null;
-const renoteUri = appearNote.renote ? appearNote.renote.uri : null;
+const appearNote = computed(() => isRenote ? note.value.renote as Misskey.entities.Note : note.value);
+const renoteUrl = appearNote.value.renote ? appearNote.value.renote.url : null;
+const renoteUri = appearNote.value.renote ? appearNote.value.renote.uri : null;
-const isMyRenote = $i && ($i.id === note.userId);
+const isMyRenote = $i && ($i.id === note.value.userId);
const showContent = ref(defaultStore.state.uncollapseCW);
-const parsed = $computed(() => appearNote.text ? mfm.parse(appearNote.text) : null);
-const urls = $computed(() => parsed ? extractUrlFromMfm(parsed).filter(u => u !== renoteUrl && u !== renoteUri) : null);
-const animated = $computed(() => parsed ? checkAnimationFromMfm(parsed) : null);
-const allowAnim = ref(defaultStore.state.advancedMfm && defaultStore.state.animatedMfm ? true : false);
-const isLong = shouldCollapsed(appearNote, urls ?? []);
-const collapsed = defaultStore.state.expandLongNote && appearNote.cw == null ? false : ref(appearNote.cw == null && isLong);
+const parsed = computed(() => appearNote.value.text ? mfm.parse(appearNote.value.text).filter(u => u !== renoteUrl && u !== renoteUri) : null);
+const urls = computed(() => parsed.value ? extractUrlFromMfm(parsed.value) : null);
+const isLong = shouldCollapsed(appearNote.value, urls.value ?? []);
+const collapsed = defaultStore.state.expandLongNote && appearNote.value.cw == null ? false : ref(appearNote.value.cw == null && isLong);
const isDeleted = ref(false);
const renoted = ref(false);
-const muted = ref($i ? checkWordMute(appearNote, $i, $i.mutedWords) : false);
+const muted = ref(checkMute(appearNote.value, $i?.mutedWords));
+const hardMuted = ref(props.withHardMute && checkMute(appearNote.value, $i?.hardMutedWords));
const translation = ref<any>(null);
const translating = ref(false);
-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));
-let renoteCollapsed = $ref(defaultStore.state.collapseRenotes && isRenote && (($i && ($i.id === note.userId || $i.id === appearNote.userId)) || (appearNote.myReaction != null)));
+const showTicker = (defaultStore.state.instanceTicker === 'always') || (defaultStore.state.instanceTicker === 'remote' && appearNote.value.user.instance);
+const canRenote = computed(() => ['public', 'home'].includes(appearNote.value.visibility) || (appearNote.value.visibility === 'followers' && appearNote.value.userId === $i.id));
+const renoteCollapsed = ref(defaultStore.state.collapseRenotes && isRenote && (($i && ($i.id === note.value.userId || $i.id === appearNote.value.userId)) || (appearNote.value.myReaction != null)));
const defaultLike = computed(() => defaultStore.state.like ? defaultStore.state.like : null);
+const animated = computed(() => parsed.value ? checkAnimationFromMfm(parsed.value) : null);
+const allowAnim = ref(defaultStore.state.advancedMfm && defaultStore.state.animatedMfm ? true : false);
+
+function checkMute(note: Misskey.entities.Note, mutedWords: Array<string | string[]> | undefined | null): boolean {
+ if (mutedWords == null) return false;
+
+ if (checkWordMute(note, $i, mutedWords)) return true;
+ if (note.reply && checkWordMute(note.reply, $i, mutedWords)) return true;
+ if (note.renote && checkWordMute(note.renote, $i, mutedWords)) return true;
+ return false;
+}
const keymap = {
'r': () => reply(true),
@@ -302,20 +321,20 @@ const keymap = {
provide('react', (reaction: string) => {
os.api('notes/reactions/create', {
- noteId: appearNote.id,
+ noteId: appearNote.value.id,
reaction: reaction,
});
});
if (props.mock) {
watch(() => props.note, (to) => {
- note = deepClone(to);
+ note.value = deepClone(to);
}, { deep: true });
} else {
useNoteCapture({
rootEl: el,
- note: $$(appearNote),
- pureNote: $$(note),
+ note: appearNote,
+ pureNote: note,
isDeletedRef: isDeleted,
});
}
@@ -323,7 +342,7 @@ if (props.mock) {
if (!props.mock) {
useTooltip(renoteButton, async (showing) => {
const renotes = await os.api('notes/renotes', {
- noteId: appearNote.id,
+ noteId: appearNote.value.id,
limit: 11,
});
@@ -334,14 +353,14 @@ if (!props.mock) {
os.popup(MkUsersTooltip, {
showing,
users,
- count: appearNote.renoteCount,
+ count: appearNote.value.renoteCount,
targetElement: renoteButton.value,
}, {}, 'closed');
});
useTooltip(quoteButton, async (showing) => {
const renotes = await os.api('notes/renotes', {
- noteId: appearNote.id,
+ noteId: appearNote.value.id,
limit: 11,
quote: true,
});
@@ -353,14 +372,14 @@ if (!props.mock) {
os.popup(MkUsersTooltip, {
showing,
users,
- count: appearNote.renoteCount,
+ count: appearNote.value.renoteCount,
targetElement: quoteButton.value,
}, {}, 'closed');
});
if ($i) {
- os.api("notes/renotes", {
- noteId: appearNote.id,
+ os.api('notes/renotes', {
+ noteId: appearNote.value.id,
userId: $i.id,
limit: 1,
}).then((res) => {
@@ -420,7 +439,7 @@ function renote(visibility: Visibility | 'local') {
pleaseLogin();
showMovedDialog();
- if (appearNote.channel) {
+ if (appearNote.value.channel) {
const el = renoteButton.value as HTMLElement | null | undefined;
if (el) {
const rect = el.getBoundingClientRect();
@@ -431,14 +450,14 @@ function renote(visibility: Visibility | 'local') {
if (!props.mock) {
os.api('notes/create', {
- renoteId: appearNote.id,
- channelId: appearNote.channelId,
+ renoteId: appearNote.value.id,
+ channelId: appearNote.value.channelId,
}).then(() => {
os.toast(i18n.ts.renoted);
renoted.value = true;
});
}
- } else if (!appearNote.channel || appearNote.channel?.allowRenoteToExternal) {
+ } else if (!appearNote.value.channel || appearNote.value.channel?.allowRenoteToExternal) {
const el = renoteButton.value as HTMLElement | null | undefined;
if (el) {
const rect = el.getBoundingClientRect();
@@ -450,16 +469,16 @@ function renote(visibility: Visibility | 'local') {
const configuredVisibility = defaultStore.state.rememberNoteVisibility ? defaultStore.state.visibility : defaultStore.state.defaultNoteVisibility;
const localOnlySetting = defaultStore.state.rememberNoteVisibility ? defaultStore.state.localOnly : defaultStore.state.defaultNoteLocalOnly;
- let noteVisibility = visibility === 'local' || visibility === 'specified' ? smallerVisibility(appearNote.visibility, configuredVisibility) : smallerVisibility(visibility, configuredVisibility);
- if (appearNote.channel?.isSensitive) {
- noteVisibility = smallerVisibility(visibility === 'local' || visibility === 'specified' ? appearNote.visibility : visibility, 'home');
+ let noteVisibility = visibility === 'local' || visibility === 'specified' ? smallerVisibility(appearNote.value.visibility, configuredVisibility) : smallerVisibility(visibility, configuredVisibility);
+ if (appearNote.value.channel?.isSensitive) {
+ noteVisibility = smallerVisibility(visibility === 'local' || visibility === 'specified' ? appearNote.value.visibility : visibility, 'home');
}
if (!props.mock) {
os.api('notes/create', {
localOnly: visibility === 'local' ? true : localOnlySetting,
visibility: noteVisibility,
- renoteId: appearNote.id,
+ renoteId: appearNote.value.id,
}).then(() => {
os.toast(i18n.ts.renoted);
renoted.value = true;
@@ -475,13 +494,13 @@ function quote() {
return;
}
- if (appearNote.channel) {
+ if (appearNote.value.channel) {
os.post({
- renote: appearNote,
- channel: appearNote.channel,
+ renote: appearNote.value,
+ channel: appearNote.value.channel,
}).then(() => {
- os.api("notes/renotes", {
- noteId: appearNote.id,
+ os.api('notes/renotes', {
+ noteId: appearNote.value.id,
userId: $i.id,
limit: 1,
quote: true,
@@ -500,10 +519,10 @@ function quote() {
});
} else {
os.post({
- renote: appearNote,
+ renote: appearNote.value,
}).then(() => {
- os.api("notes/renotes", {
- noteId: appearNote.id,
+ os.api('notes/renotes', {
+ noteId: appearNote.value.id,
userId: $i.id,
limit: 1,
quote: true,
@@ -529,8 +548,8 @@ function reply(viaKeyboard = false): void {
return;
}
os.post({
- reply: appearNote,
- channel: appearNote.channel,
+ reply: appearNote.value,
+ channel: appearNote.value.channel,
animation: !viaKeyboard,
}, () => {
focus();
@@ -544,7 +563,7 @@ function like(): void {
return;
}
os.api('notes/like', {
- noteId: appearNote.id,
+ noteId: appearNote.value.id,
override: defaultLike.value,
});
const el = likeButton.value as HTMLElement | null | undefined;
@@ -559,13 +578,15 @@ function like(): void {
function react(viaKeyboard = false): void {
pleaseLogin();
showMovedDialog();
- if (appearNote.reactionAcceptance === 'likeOnly') {
+ if (appearNote.value.reactionAcceptance === 'likeOnly') {
+ sound.play('reaction');
+
if (props.mock) {
return;
}
os.api('notes/like', {
- noteId: appearNote.id,
+ noteId: appearNote.value.id,
override: defaultLike.value,
});
const el = reactButton.value as HTMLElement | null | undefined;
@@ -578,16 +599,18 @@ function react(viaKeyboard = false): void {
} else {
blur();
reactionPicker.show(reactButton.value, reaction => {
+ sound.play('reaction');
+
if (props.mock) {
emit('reaction', reaction);
return;
}
os.api('notes/reactions/create', {
- noteId: appearNote.id,
+ noteId: appearNote.value.id,
reaction: reaction,
});
- if (appearNote.text && appearNote.text.length > 100 && (Date.now() - new Date(appearNote.createdAt).getTime() < 1000 * 3)) {
+ if (appearNote.value.text && appearNote.value.text.length > 100 && (Date.now() - new Date(appearNote.value.createdAt).getTime() < 1000 * 3)) {
claimAchievement('reactWithoutRead');
}
}, () => {
@@ -614,8 +637,8 @@ function undoRenote(note) : void {
if (props.mock) {
return;
}
- os.api("notes/unrenote", {
- noteId: note.id
+ os.api('notes/unrenote', {
+ noteId: note.id,
});
os.toast(i18n.ts.rmboost);
renoted.value = false;
@@ -649,7 +672,7 @@ function onContextmenu(ev: MouseEvent): void {
ev.preventDefault();
react();
} else {
- const { menu, cleanup } = getNoteMenu({ note: note, translating, translation, menuButton, isDeleted, currentClip: currentClip?.value });
+ const { menu, cleanup } = getNoteMenu({ note: note.value, translating, translation, menuButton, isDeleted, currentClip: currentClip?.value });
os.contextMenu(menu, ev).then(focus).finally(cleanup);
}
}
@@ -659,14 +682,14 @@ function menu(viaKeyboard = false): void {
return;
}
- const { menu, cleanup } = getNoteMenu({ note: note, translating, translation, menuButton, isDeleted, currentClip: currentClip?.value });
+ const { menu, cleanup } = getNoteMenu({ note: note.value, translating, translation, menuButton, isDeleted, currentClip: currentClip?.value });
os.popupMenu(menu, menuButton.value, {
viaKeyboard,
}).then(focus).finally(cleanup);
}
async function menuVersions(viaKeyboard = false): Promise<void> {
- const { menu, cleanup } = await getNoteVersionsMenu({ note: note, menuVersionsButton });
+ const { menu, cleanup } = await getNoteVersionsMenu({ note: note.value, menuVersionsButton });
os.popupMenu(menu, menuVersionsButton.value, {
viaKeyboard,
}).then(focus).finally(cleanup);
@@ -677,7 +700,7 @@ async function clip() {
return;
}
- os.popupMenu(await getNoteClipMenu({ note: note, isDeleted, currentClip: currentClip?.value }), clipButton.value).then(focus);
+ os.popupMenu(await getNoteClipMenu({ note: note.value, isDeleted, currentClip: currentClip?.value }), clipButton.value).then(focus);
}
function showRenoteMenu(viaKeyboard = false): void {
@@ -692,7 +715,7 @@ function showRenoteMenu(viaKeyboard = false): void {
danger: true,
action: () => {
os.api('notes/delete', {
- noteId: note.id,
+ noteId: note.value.id,
});
isDeleted.value = true;
},
@@ -702,17 +725,17 @@ function showRenoteMenu(viaKeyboard = false): void {
if (isMyRenote) {
pleaseLogin();
os.popupMenu([
- getCopyNoteLinkMenu(note, i18n.ts.copyLinkRenote),
- null,
+ getCopyNoteLinkMenu(note.value, i18n.ts.copyLinkRenote),
+ { type: 'divider' },
getUnrenote(),
], renoteTime.value, {
viaKeyboard: viaKeyboard,
});
} else {
os.popupMenu([
- getCopyNoteLinkMenu(note, i18n.ts.copyLinkRenote),
- null,
- getAbuseNoteMenu(note, i18n.ts.reportAbuseRenote),
+ getCopyNoteLinkMenu(note.value, i18n.ts.copyLinkRenote),
+ { type: 'divider' },
+ getAbuseNoteMenu(note.value, i18n.ts.reportAbuseRenote),
$i.isModerator || $i.isAdmin ? getUnrenote() : undefined,
], renoteTime.value, {
viaKeyboard: viaKeyboard,
@@ -750,7 +773,7 @@ function focusAfter() {
function readPromo() {
os.api('promo/read', {
- noteId: appearNote.id,
+ noteId: appearNote.value.id,
});
isDeleted.value = true;
}
diff --git a/packages/frontend/src/components/SkNoteDetailed.vue b/packages/frontend/src/components/SkNoteDetailed.vue
index 4699eba8f6..8bf9e244e0 100644
--- a/packages/frontend/src/components/SkNoteDetailed.vue
+++ b/packages/frontend/src/components/SkNoteDetailed.vue
@@ -78,7 +78,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<div :class="$style.noteContent">
<p v-if="appearNote.cw != null" :class="$style.cw">
<Mfm v-if="appearNote.cw != ''" style="margin-right: 8px;" :text="appearNote.cw" :author="appearNote.user" :nyaize="'respect'"/>
- <MkCwButton v-model="showContent" :note="appearNote"/>
+ <MkCwButton v-model="showContent" :text="appearNote.text" :files="appearNote.files" :poll="appearNote.poll"/>
</p>
<div v-show="appearNote.cw == null || showContent">
<span v-if="appearNote.isHidden" style="opacity: 0.5">({{ i18n.ts.private }})</span>
@@ -101,8 +101,8 @@ SPDX-License-Identifier: AGPL-3.0-only
<Mfm :text="translation.text" :author="appearNote.user" :nyaize="'respect'" :emojiUrls="appearNote.emojis"/>
</div>
</div>
- <MkButton v-if="!allowAnim && animated" :class="$style.playMFMButton" :small="true" @click="animatedMFM()" v-on:click.stop><i class="ph-play ph-bold ph-lg "></i> {{ i18n.ts._animatedMFM.play }}</MkButton>
- <MkButton v-else-if="!defaultStore.state.animatedMfm && allowAnim && animated" :class="$style.playMFMButton" :small="true" @click="animatedMFM()" v-on:click.stop><i class="ph-stop ph-bold ph-lg "></i> {{ i18n.ts._animatedMFM.stop }}</MkButton>
+ <MkButton v-if="!allowAnim && animated" :class="$style.playMFMButton" :small="true" @click="animatedMFM()" @click.stop><i class="ph-play ph-bold ph-lg "></i> {{ i18n.ts._animatedMFM.play }}</MkButton>
+ <MkButton v-else-if="!defaultStore.state.animatedMfm && allowAnim && animated" :class="$style.playMFMButton" :small="true" @click="animatedMFM()" @click.stop><i class="ph-stop ph-bold ph-lg "></i> {{ i18n.ts._animatedMFM.stop }}</MkButton>
<div v-if="appearNote.files.length > 0">
<MkMediaList :mediaList="appearNote.files"/>
</div>
@@ -245,6 +245,7 @@ import { checkWordMute } from '@/scripts/check-word-mute.js';
import { userPage } from '@/filters/user.js';
import { notePage } from '@/filters/note.js';
import * as os from '@/os.js';
+import * as sound from '@/scripts/sound.js';
import { defaultStore, noteViewInterruptors } from '@/store.js';
import { reactionPicker } from '@/scripts/reaction-picker.js';
import { extractUrlFromMfm } from '@/scripts/extract-url-from-mfm.js';
@@ -256,12 +257,11 @@ import { useNoteCapture } from '@/scripts/use-note-capture.js';
import { deepClone } from '@/scripts/clone.js';
import { useTooltip } from '@/scripts/use-tooltip.js';
import { claimAchievement } from '@/scripts/achievements.js';
-import { MenuItem } from '@/types/menu.js';
import { checkAnimationFromMfm } from '@/scripts/check-animated-mfm.js';
import MkRippleEffect from '@/components/MkRippleEffect.vue';
import { showMovedDialog } from '@/scripts/show-moved-dialog.js';
import MkUserCardMini from '@/components/MkUserCardMini.vue';
-import MkPagination, { Paging } from '@/components/MkPagination.vue';
+import MkPagination from '@/components/MkPagination.vue';
import MkReactionIcon from '@/components/MkReactionIcon.vue';
import MkButton from '@/components/MkButton.vue';
@@ -272,12 +272,12 @@ const props = defineProps<{
const inChannel = inject('inChannel', null);
-let note = $ref(deepClone(props.note));
+const note = ref(deepClone(props.note));
// plugin
if (noteViewInterruptors.length > 0) {
onMounted(async () => {
- let result: Misskey.entities.Note | null = deepClone(note);
+ let result: Misskey.entities.Note | null = deepClone(note.value);
for (const interruptor of noteViewInterruptors) {
try {
result = await interruptor.handler(result);
@@ -289,15 +289,15 @@ if (noteViewInterruptors.length > 0) {
console.error(err);
}
}
- note = result;
+ note.value = result;
});
}
const isRenote = (
- note.renote != null &&
- note.text == null &&
- note.fileIds.length === 0 &&
- note.poll == null
+ note.value.renote != null &&
+ note.value.text == null &&
+ note.value.fileIds.length === 0 &&
+ note.value.poll == null
);
const el = shallowRef<HTMLElement>();
@@ -309,26 +309,25 @@ const reactButton = shallowRef<HTMLElement>();
const quoteButton = shallowRef<HTMLElement>();
const clipButton = shallowRef<HTMLElement>();
const likeButton = shallowRef<HTMLElement>();
-let appearNote = $computed(() => isRenote ? note.renote as Misskey.entities.Note : note);
-const renoteUrl = appearNote.renote ? appearNote.renote.url : null;
-const renoteUri = appearNote.renote ? appearNote.renote.uri : null;
-
-const isMyRenote = $i && ($i.id === note.userId);
+const appearNote = computed(() => isRenote ? note.value.renote as Misskey.entities.Note : note.value);
+const renoteUrl = appearNote.value.renote ? appearNote.value.renote.url : null;
+const renoteUri = appearNote.value.renote ? appearNote.value.renote.uri : null;
+const isMyRenote = $i && ($i.id === note.value.userId);
const showContent = ref(defaultStore.state.uncollapseCW);
const isDeleted = ref(false);
const renoted = ref(false);
-const muted = ref($i ? checkWordMute(appearNote, $i, $i.mutedWords) : false);
+const muted = ref($i ? checkWordMute(appearNote.value, $i, $i.mutedWords) : false);
const translation = ref(null);
const translating = ref(false);
-const parsed = $computed(() => appearNote.text ? mfm.parse(appearNote.text) : null);
+const parsed = appearNote.value.text ? mfm.parse(appearNote.value.text) : null;
const urls = parsed ? extractUrlFromMfm(parsed).filter(u => u !== renoteUrl && u !== renoteUri) : null;
-const animated = $computed(() => parsed ? checkAnimationFromMfm(parsed) : null);
+const animated = computed(() => parsed ? checkAnimationFromMfm(parsed) : null);
const allowAnim = ref(defaultStore.state.advancedMfm && defaultStore.state.animatedMfm ? true : 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.value.user.instance);
const conversation = ref<Misskey.entities.Note[]>([]);
const replies = 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.value.visibility) || appearNote.value.userId === $i.id);
const defaultLike = computed(() => defaultStore.state.like ? defaultStore.state.like : null);
watch(() => props.expandAllCws, (expandAllCws) => {
@@ -336,8 +335,8 @@ watch(() => props.expandAllCws, (expandAllCws) => {
});
if ($i) {
- os.api("notes/renotes", {
- noteId: appearNote.id,
+ os.api('notes/renotes', {
+ noteId: appearNote.value.id,
userId: $i.id,
limit: 1,
}).then((res) => {
@@ -356,41 +355,41 @@ const keymap = {
provide('react', (reaction: string) => {
os.api('notes/reactions/create', {
- noteId: appearNote.id,
+ noteId: appearNote.value.id,
reaction: reaction,
});
});
-let tab = $ref('replies');
-let reactionTabType = $ref(null);
+const tab = ref('replies');
+const reactionTabType = ref(null);
-const renotesPagination = $computed(() => ({
+const renotesPagination = computed(() => ({
endpoint: 'notes/renotes',
limit: 10,
params: {
- noteId: appearNote.id,
+ noteId: appearNote.value.id,
},
}));
-const reactionsPagination = $computed(() => ({
+const reactionsPagination = computed(() => ({
endpoint: 'notes/reactions',
limit: 10,
params: {
- noteId: appearNote.id,
- type: reactionTabType,
+ noteId: appearNote.value.id,
+ type: reactionTabType.value,
},
}));
useNoteCapture({
rootEl: el,
- note: $$(appearNote),
- pureNote: $$(note),
+ note: appearNote,
+ pureNote: note,
isDeletedRef: isDeleted,
});
useTooltip(renoteButton, async (showing) => {
const renotes = await os.api('notes/renotes', {
- noteId: appearNote.id,
+ noteId: appearNote.value.id,
limit: 11,
});
@@ -401,14 +400,14 @@ useTooltip(renoteButton, async (showing) => {
os.popup(MkUsersTooltip, {
showing,
users,
- count: appearNote.renoteCount,
+ count: appearNote.value.renoteCount,
targetElement: renoteButton.value,
}, {}, 'closed');
});
useTooltip(quoteButton, async (showing) => {
const renotes = await os.api('notes/renotes', {
- noteId: appearNote.id,
+ noteId: appearNote.value.id,
limit: 11,
quote: true,
});
@@ -420,7 +419,7 @@ useTooltip(quoteButton, async (showing) => {
os.popup(MkUsersTooltip, {
showing,
users,
- count: appearNote.renoteCount,
+ count: appearNote.value.renoteCount,
targetElement: quoteButton.value,
}, {}, 'closed');
});
@@ -475,7 +474,7 @@ function renote(visibility: Visibility | 'local') {
pleaseLogin();
showMovedDialog();
- if (appearNote.channel) {
+ if (appearNote.value.channel) {
const el = renoteButton.value as HTMLElement | null | undefined;
if (el) {
const rect = el.getBoundingClientRect();
@@ -485,13 +484,13 @@ function renote(visibility: Visibility | 'local') {
}
os.api('notes/create', {
- renoteId: appearNote.id,
- channelId: appearNote.channelId,
+ renoteId: appearNote.value.id,
+ channelId: appearNote.value.channelId,
}).then(() => {
os.toast(i18n.ts.renoted);
renoted.value = true;
});
- } else if (!appearNote.channel || appearNote.channel?.allowRenoteToExternal) {
+ } else if (!appearNote.value.channel || appearNote.value.channel?.allowRenoteToExternal) {
const el = renoteButton.value as HTMLElement | null | undefined;
if (el) {
const rect = el.getBoundingClientRect();
@@ -503,15 +502,15 @@ function renote(visibility: Visibility | 'local') {
const configuredVisibility = defaultStore.state.rememberNoteVisibility ? defaultStore.state.visibility : defaultStore.state.defaultNoteVisibility;
const localOnlySetting = defaultStore.state.rememberNoteVisibility ? defaultStore.state.localOnly : defaultStore.state.defaultNoteLocalOnly;
- let noteVisibility = visibility === 'local' || visibility === 'specified' ? smallerVisibility(appearNote.visibility, configuredVisibility) : smallerVisibility(visibility, configuredVisibility);
- if (appearNote.channel?.isSensitive) {
- noteVisibility = smallerVisibility(visibility === 'local' || visibility === 'specified' ? appearNote.visibility : visibility, 'home');
+ let noteVisibility = visibility === 'local' || visibility === 'specified' ? smallerVisibility(appearNote.value.visibility, configuredVisibility) : smallerVisibility(visibility, configuredVisibility);
+ if (appearNote.value.channel?.isSensitive) {
+ noteVisibility = smallerVisibility(visibility === 'local' || visibility === 'specified' ? appearNote.value.visibility : visibility, 'home');
}
os.api('notes/create', {
localOnly: visibility === 'local' ? true : localOnlySetting,
visibility: noteVisibility,
- renoteId: appearNote.id,
+ renoteId: appearNote.value.id,
}).then(() => {
os.toast(i18n.ts.renoted);
renoted.value = true;
@@ -523,13 +522,13 @@ function quote() {
pleaseLogin();
showMovedDialog();
- if (appearNote.channel) {
+ if (appearNote.value.channel) {
os.post({
- renote: appearNote,
- channel: appearNote.channel,
+ renote: appearNote.value,
+ channel: appearNote.value.channel,
}).then(() => {
- os.api("notes/renotes", {
- noteId: appearNote.id,
+ os.api('notes/renotes', {
+ noteId: appearNote.value.id,
userId: $i.id,
limit: 1,
quote: true,
@@ -548,10 +547,10 @@ function quote() {
});
} else {
os.post({
- renote: appearNote,
+ renote: appearNote.value,
}).then(() => {
- os.api("notes/renotes", {
- noteId: appearNote.id,
+ os.api('notes/renotes', {
+ noteId: appearNote.value.id,
userId: $i.id,
limit: 1,
quote: true,
@@ -575,8 +574,8 @@ function reply(viaKeyboard = false): void {
pleaseLogin();
showMovedDialog();
os.post({
- reply: appearNote,
- channel: appearNote.channel,
+ reply: appearNote.value,
+ channel: appearNote.value.channel,
animation: !viaKeyboard,
}, () => {
focus();
@@ -586,9 +585,9 @@ function reply(viaKeyboard = false): void {
function react(viaKeyboard = false): void {
pleaseLogin();
showMovedDialog();
- if (appearNote.reactionAcceptance === 'likeOnly') {
+ if (appearNote.value.reactionAcceptance === 'likeOnly') {
os.api('notes/like', {
- noteId: appearNote.id,
+ noteId: appearNote.value.id,
override: defaultLike.value,
});
const el = reactButton.value as HTMLElement | null | undefined;
@@ -601,11 +600,13 @@ function react(viaKeyboard = false): void {
} else {
blur();
reactionPicker.show(reactButton.value, reaction => {
+ sound.play('reaction');
+
os.api('notes/reactions/create', {
- noteId: appearNote.id,
+ noteId: appearNote.value.id,
reaction: reaction,
});
- if (appearNote.text && appearNote.text.length > 100 && (Date.now() - new Date(appearNote.createdAt).getTime() < 1000 * 3)) {
+ if (appearNote.value.text && appearNote.value.text.length > 100 && (Date.now() - new Date(appearNote.value.createdAt).getTime() < 1000 * 3)) {
claimAchievement('reactWithoutRead');
}
}, () => {
@@ -618,7 +619,7 @@ function like(): void {
pleaseLogin();
showMovedDialog();
os.api('notes/like', {
- noteId: appearNote.id,
+ noteId: appearNote.value.id,
override: defaultLike.value,
});
const el = likeButton.value as HTMLElement | null | undefined;
@@ -640,8 +641,8 @@ function undoReact(note): void {
function undoRenote() : void {
if (!renoted.value) return;
- os.api("notes/unrenote", {
- noteId: appearNote.id,
+ os.api('notes/unrenote', {
+ noteId: appearNote.value.id,
});
os.toast(i18n.ts.rmboost);
renoted.value = false;
@@ -669,27 +670,27 @@ function onContextmenu(ev: MouseEvent): void {
ev.preventDefault();
react();
} else {
- const { menu, cleanup } = getNoteMenu({ note: note, translating, translation, menuButton, isDeleted });
+ const { menu, cleanup } = getNoteMenu({ note: note.value, translating, translation, menuButton, isDeleted });
os.contextMenu(menu, ev).then(focus).finally(cleanup);
}
}
function menu(viaKeyboard = false): void {
- const { menu, cleanup } = getNoteMenu({ note: note, translating, translation, menuButton, isDeleted });
+ const { menu, cleanup } = getNoteMenu({ note: note.value, translating, translation, menuButton, isDeleted });
os.popupMenu(menu, menuButton.value, {
viaKeyboard,
}).then(focus).finally(cleanup);
}
async function menuVersions(viaKeyboard = false): Promise<void> {
- const { menu, cleanup } = await getNoteVersionsMenu({ note: note, menuVersionsButton });
+ const { menu, cleanup } = await getNoteVersionsMenu({ note: note.value, menuVersionsButton });
os.popupMenu(menu, menuVersionsButton.value, {
viaKeyboard,
}).then(focus).finally(cleanup);
}
async function clip() {
- os.popupMenu(await getNoteClipMenu({ note: note, isDeleted }), clipButton.value).then(focus);
+ os.popupMenu(await getNoteClipMenu({ note: note.value, isDeleted }), clipButton.value).then(focus);
}
function showRenoteMenu(viaKeyboard = false): void {
@@ -701,7 +702,7 @@ function showRenoteMenu(viaKeyboard = false): void {
danger: true,
action: () => {
os.api('notes/delete', {
- noteId: note.id,
+ noteId: note.value.id,
});
isDeleted.value = true;
},
@@ -723,7 +724,7 @@ const repliesLoaded = ref(false);
function loadReplies() {
repliesLoaded.value = true;
os.api('notes/children', {
- noteId: appearNote.id,
+ noteId: appearNote.value.id,
limit: 30,
showQuotes: false,
}).then(res => {
@@ -738,7 +739,7 @@ const quotesLoaded = ref(false);
function loadQuotes() {
quotesLoaded.value = true;
os.api('notes/renotes', {
- noteId: appearNote.id,
+ noteId: appearNote.value.id,
limit: 30,
quote: true,
}).then(res => {
@@ -753,13 +754,13 @@ const conversationLoaded = ref(false);
function loadConversation() {
conversationLoaded.value = true;
os.api('notes/conversation', {
- noteId: appearNote.replyId,
+ noteId: appearNote.value.replyId,
}).then(res => {
conversation.value = res.reverse();
});
}
-if (appearNote.reply && appearNote.reply.replyId && defaultStore.state.autoloadConversation) loadConversation();
+if (appearNote.value.reply && appearNote.value.reply.replyId && defaultStore.state.autoloadConversation) loadConversation();
function animatedMFM() {
if (allowAnim.value) {
diff --git a/packages/frontend/src/components/SkNoteSimple.vue b/packages/frontend/src/components/SkNoteSimple.vue
index 05a19e291d..fe12baedeb 100644
--- a/packages/frontend/src/components/SkNoteSimple.vue
+++ b/packages/frontend/src/components/SkNoteSimple.vue
@@ -11,7 +11,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<div>
<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"/>
- <MkCwButton v-model="showContent" :note="note" v-on:click.stop/>
+ <MkCwButton v-model="showContent" :text="note.text" :files="note.files" :poll="note.poll" @click.stop/>
</p>
<div v-show="note.cw == null || showContent">
<MkSubNoteContent :hideFiles="hideFiles" :class="$style.text" :note="note"/>
@@ -22,12 +22,11 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { watch } from 'vue';
+import { watch, ref } from 'vue';
import * as Misskey from 'misskey-js';
import MkNoteHeader from '@/components/MkNoteHeader.vue';
import MkSubNoteContent from '@/components/MkSubNoteContent.vue';
import MkCwButton from '@/components/MkCwButton.vue';
-import { $i } from '@/account.js';
import { defaultStore } from '@/store.js';
const props = defineProps<{
@@ -36,10 +35,10 @@ const props = defineProps<{
hideFiles?: boolean;
}>();
-let showContent = $ref(defaultStore.state.uncollapseCW);
+let showContent = ref(defaultStore.state.uncollapseCW);
watch(() => props.expandAllCws, (expandAllCws) => {
- if (expandAllCws !== showContent) showContent = expandAllCws;
+ if (expandAllCws !== showContent.value) showContent.value = expandAllCws;
});
</script>
diff --git a/packages/frontend/src/components/SkNoteSub.vue b/packages/frontend/src/components/SkNoteSub.vue
index dd4abe8f58..fc30dc87aa 100644
--- a/packages/frontend/src/components/SkNoteSub.vue
+++ b/packages/frontend/src/components/SkNoteSub.vue
@@ -21,7 +21,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<div :class="$style.content">
<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'"/>
- <MkCwButton v-model="showContent" :note="note"/>
+ <MkCwButton v-model="showContent" :text="note.text" :files="note.files" :poll="note.poll"/>
</p>
<div v-show="note.cw == null || showContent">
<MkSubNoteContent :class="$style.text" :note="note" :translating="translating" :translation="translation"/>
@@ -101,15 +101,14 @@ import { notePage } from '@/filters/note.js';
import * as os from '@/os.js';
import { i18n } from '@/i18n.js';
import { $i } from '@/account.js';
-import { userPage } from "@/filters/user.js";
-import { checkWordMute } from "@/scripts/check-word-mute.js";
-import { defaultStore } from "@/store.js";
+import { userPage } from '@/filters/user.js';
+import { checkWordMute } from '@/scripts/check-word-mute.js';
+import { defaultStore } from '@/store.js';
import { pleaseLogin } from '@/scripts/please-login.js';
import { showMovedDialog } from '@/scripts/show-moved-dialog.js';
import MkRippleEffect from '@/components/MkRippleEffect.vue';
import { reactionPicker } from '@/scripts/reaction-picker.js';
import { claimAchievement } from '@/scripts/achievements.js';
-import type { MenuItem } from '@/types/menu.js';
import { getNoteMenu } from '@/scripts/get-note-menu.js';
import { useNoteCapture } from '@/scripts/use-note-capture.js';
@@ -140,7 +139,7 @@ const quoteButton = shallowRef<HTMLElement>();
const menuButton = 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 isRenote = (
@@ -152,13 +151,13 @@ const isRenote = (
useNoteCapture({
rootEl: el,
- note: $$(appearNote),
+ note: appearNote,
isDeletedRef: isDeleted,
});
if ($i) {
- os.api("notes/renotes", {
- noteId: appearNote.id,
+ os.api('notes/renotes', {
+ noteId: appearNote.value.id,
userId: $i.id,
limit: 1,
}).then((res) => {
@@ -239,8 +238,8 @@ function undoReact(note): void {
function undoRenote() : void {
if (!renoted.value) return;
- os.api("notes/unrenote", {
- noteId: appearNote.id,
+ os.api('notes/unrenote', {
+ noteId: appearNote.value.id,
});
os.toast(i18n.ts.rmboost);
renoted.value = false;
@@ -254,13 +253,13 @@ function undoRenote() : void {
}
}
-let showContent = $ref(defaultStore.state.uncollapseCW);
+let showContent = ref(defaultStore.state.uncollapseCW);
watch(() => props.expandAllCws, (expandAllCws) => {
- if (expandAllCws !== showContent) showContent = expandAllCws;
+ if (expandAllCws !== showContent.value) showContent.value = expandAllCws;
});
-let replies: Misskey.entities.Note[] = $ref([]);
+let replies = ref<Misskey.entities.Note[]>([]);
function boostVisibility() {
os.popupMenu([
@@ -302,7 +301,7 @@ function renote(visibility: 'public' | 'home' | 'followers' | 'specified' | 'loc
pleaseLogin();
showMovedDialog();
- if (appearNote.channel) {
+ if (appearNote.value.channel) {
const el = renoteButton.value as HTMLElement | null | undefined;
if (el) {
const rect = el.getBoundingClientRect();
@@ -342,12 +341,12 @@ function quote() {
pleaseLogin();
showMovedDialog();
- if (appearNote.channel) {
+ if (appearNote.value.channel) {
os.post({
- renote: appearNote,
- channel: appearNote.channel,
+ renote: appearNote.value,
+ channel: appearNote.value.channel,
}).then(() => {
- os.api("notes/renotes", {
+ os.api('notes/renotes', {
noteId: props.note.id,
userId: $i.id,
limit: 1,
@@ -367,9 +366,9 @@ function quote() {
});
} else {
os.post({
- renote: appearNote,
+ renote: appearNote.value,
}).then(() => {
- os.api("notes/renotes", {
+ os.api('notes/renotes', {
noteId: props.note.id,
userId: $i.id,
limit: 1,
@@ -403,7 +402,7 @@ if (props.detail) {
limit: numberOfReplies.value,
showQuotes: false,
}).then(res => {
- replies = res;
+ replies.value = res;
});
}
</script>
diff --git a/packages/frontend/src/components/SkOldNoteWindow.vue b/packages/frontend/src/components/SkOldNoteWindow.vue
index 49fbd39812..237032c9d5 100644
--- a/packages/frontend/src/components/SkOldNoteWindow.vue
+++ b/packages/frontend/src/components/SkOldNoteWindow.vue
@@ -30,7 +30,7 @@
<div :class="$style.noteContent">
<p v-if="appearNote.cw != null" :class="$style.cw">
<Mfm v-if="appearNote.cw != ''" style="margin-right: 8px;" :text="appearNote.cw" :author="appearNote.user" :nyaize="'account'"/>
- <MkCwButton v-model="showContent" :note="appearNote"/>
+ <MkCwButton v-model="showContent" :text="appearNote.text" :files="appearNote.files" :poll="appearNote.poll"/>
</p>
<div v-show="appearNote.cw == null || showContent">
<span v-if="appearNote.isHidden" style="opacity: 0.5">({{ i18n.ts.private }})</span>
@@ -76,7 +76,7 @@
</template>
<script lang="ts" setup>
-import { inject, onMounted, ref, shallowRef } from 'vue';
+import { inject, onMounted, ref, shallowRef, computed } from 'vue';
import * as mfm from '@sharkey/sfm-js';
import * as Misskey from 'misskey-js';
import MkNoteSimple from '@/components/MkNoteSimple.vue';
@@ -89,7 +89,6 @@ import MkInstanceTicker from '@/components/MkInstanceTicker.vue';
import { userPage } from '@/filters/user.js';
import { defaultStore, noteViewInterruptors } from '@/store.js';
import { extractUrlFromMfm } from '@/scripts/extract-url-from-mfm.js';
-import { $i } from '@/account.js';
import { i18n } from '@/i18n.js';
import { deepClone } from '@/scripts/clone.js';
import { dateTimeFormat } from '@/scripts/intl-const.js';
@@ -106,42 +105,42 @@ const emit = defineEmits<{
const inChannel = inject('inChannel', null);
-let note = $ref(deepClone(props.note));
+let note = ref(deepClone(props.note));
// plugin
if (noteViewInterruptors.length > 0) {
onMounted(async () => {
- let result = deepClone(note);
+ let result = deepClone(note.value);
for (const interruptor of noteViewInterruptors) {
result = await interruptor.handler(result);
}
- note = result;
+ note.value = result;
});
}
const replaceContent = () => {
- props.oldText ? note.text = props.oldText : undefined;
- note.createdAt = props.updatedAt;
+ props.oldText ? note.value.text = props.oldText : undefined;
+ note.value.createdAt = props.updatedAt;
};
replaceContent();
const isRenote = (
- note.renote != null &&
- note.text == null &&
- note.fileIds.length === 0 &&
- note.poll == null
+ note.value.renote != null &&
+ note.value.text == null &&
+ note.value.fileIds.length === 0 &&
+ note.value.poll == null
);
const el = shallowRef<HTMLElement>();
-let appearNote = $computed(() => isRenote ? note.renote as Misskey.entities.Note : note);
-const renoteUrl = appearNote.renote ? appearNote.renote.url : null;
-const renoteUri = appearNote.renote ? appearNote.renote.uri : null;
+let appearNote = computed(() => isRenote ? note.value.renote as Misskey.entities.Note : note);
+const renoteUrl = appearNote.value.renote ? appearNote.value.renote.url : null;
+const renoteUri = appearNote.value.renote ? appearNote.value.renote.uri : null;
const showContent = ref(false);
const translation = ref(null);
const translating = ref(false);
-const urls = appearNote.text ? extractUrlFromMfm(mfm.parse(appearNote.text)).filter(u => u !== renoteUrl && u !== renoteUri) : null;
-const showTicker = (defaultStore.state.instanceTicker === 'always') || (defaultStore.state.instanceTicker === 'remote' && appearNote.user.instance);
+const urls = appearNote.value.text ? extractUrlFromMfm(mfm.parse(appearNote.value.text)).filter(u => u !== renoteUrl && u !== renoteUri) : null;
+const showTicker = (defaultStore.state.instanceTicker === 'always') || (defaultStore.state.instanceTicker === 'remote' && appearNote.value.user.instance);
</script>
diff --git a/packages/frontend/src/components/form/section.vue b/packages/frontend/src/components/form/section.vue
index 095b24604a..6af63d1ec6 100644
--- a/packages/frontend/src/components/form/section.vue
+++ b/packages/frontend/src/components/form/section.vue
@@ -6,6 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template>
<div :class="[$style.root, { [$style.rootFirst]: first }]">
<div :class="[$style.label, { [$style.labelFirst]: first }]"><slot name="label"></slot></div>
+ <div :class="[$style.description]"><slot name="description"></slot></div>
<div :class="$style.main">
<slot></slot>
</div>
@@ -31,7 +32,7 @@ defineProps<{
.label {
font-weight: bold;
padding: 1.5em 0 0 0;
- margin: 0 0 16px 0;
+ margin: 0 0 8px 0;
&:empty {
display: none;
@@ -45,4 +46,10 @@ defineProps<{
.main {
margin: 1.5em 0 0 0;
}
+
+.description {
+ font-size: 0.85em;
+ color: var(--fgTransparentWeak);
+ margin: 0 0 8px 0;
+}
</style>
diff --git a/packages/frontend/src/components/form/suspense.vue b/packages/frontend/src/components/form/suspense.vue
index f65f8a78ff..af5daa10ff 100644
--- a/packages/frontend/src/components/form/suspense.vue
+++ b/packages/frontend/src/components/form/suspense.vue
@@ -21,7 +21,6 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { ref, watch } from 'vue';
import MkButton from '@/components/MkButton.vue';
-import { defaultStore } from '@/store.js';
import { i18n } from '@/i18n.js';
const props = defineProps<{
diff --git a/packages/frontend/src/components/global/MkA.vue b/packages/frontend/src/components/global/MkA.vue
index 7689bba7bf..e2b59869a4 100644
--- a/packages/frontend/src/components/global/MkA.vue
+++ b/packages/frontend/src/components/global/MkA.vue
@@ -4,16 +4,16 @@ SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
-<a :href="to" :class="active ? activeClass : null" @click.prevent="nav" @contextmenu.prevent.stop="onContextmenu" v-on:click.stop>
+<a :href="to" :class="active ? activeClass : null" @click.prevent="nav" @contextmenu.prevent.stop="onContextmenu" @click.stop>
<slot></slot>
</a>
</template>
<script lang="ts" setup>
+import { computed } from 'vue';
import * as os from '@/os.js';
import copyToClipboard from '@/scripts/copy-to-clipboard.js';
import { url } from '@/config.js';
-import { popout as popout_ } from '@/scripts/popout.js';
import { i18n } from '@/i18n.js';
import { useRouter } from '@/router.js';
@@ -28,7 +28,7 @@ const props = withDefaults(defineProps<{
const router = useRouter();
-const active = $computed(() => {
+const active = computed(() => {
if (props.activeClass == null) return false;
const resolved = router.resolve(props.to);
if (resolved == null) return false;
@@ -56,11 +56,11 @@ function onContextmenu(ev) {
action: () => {
router.push(props.to, 'forcePage');
},
- }, null, {
+ }, { type: 'divider' }, {
icon: 'ph-arrow-square-out ph-bold ph-lg',
text: i18n.ts.openInNewTab,
action: () => {
- window.open(props.to, '_blank');
+ window.open(props.to, '_blank', 'noopener');
},
}, {
icon: 'ph-link ph-bold ph-lg',
diff --git a/packages/frontend/src/components/global/MkAd.stories.impl.ts b/packages/frontend/src/components/global/MkAd.stories.impl.ts
index 360bc88b4a..5ae45ec58f 100644
--- a/packages/frontend/src/components/global/MkAd.stories.impl.ts
+++ b/packages/frontend/src/components/global/MkAd.stories.impl.ts
@@ -4,11 +4,8 @@
*/
/* eslint-disable @typescript-eslint/explicit-function-return-type */
-import { expect } from '@storybook/jest';
-import { userEvent, waitFor, within } from '@storybook/testing-library';
import { StoryObj } from '@storybook/vue3';
import MkAd from './MkAd.vue';
-import { i18n } from '@/i18n.js';
let lock: Promise<undefined> | undefined;
diff --git a/packages/frontend/src/components/global/MkAd.vue b/packages/frontend/src/components/global/MkAd.vue
index 3e092753a3..b3eb6d681f 100644
--- a/packages/frontend/src/components/global/MkAd.vue
+++ b/packages/frontend/src/components/global/MkAd.vue
@@ -96,7 +96,7 @@ const choseAd = (): Ad | null => {
};
const chosen = ref(choseAd());
-const shouldHide = $ref(!defaultStore.state.forceShowAds && $i && $i.policies.canHideAds && (props.specify == null));
+const shouldHide = ref(!defaultStore.state.forceShowAds && $i && $i.policies.canHideAds && (props.specify == null));
function reduceFrequency(): void {
if (chosen.value == null) return;
diff --git a/packages/frontend/src/components/global/MkAvatar.vue b/packages/frontend/src/components/global/MkAvatar.vue
index 01bf66fed5..4a876931c3 100644
--- a/packages/frontend/src/components/global/MkAvatar.vue
+++ b/packages/frontend/src/components/global/MkAvatar.vue
@@ -23,21 +23,24 @@ SPDX-License-Identifier: AGPL-3.0-only
</div>
</div>
</div>
- <img
- v-if="showDecoration && (decoration || user.avatarDecorations.length > 0)"
- :class="[$style.decoration]"
- :src="decoration?.url ?? user.avatarDecorations[0].url"
- :style="{
- rotate: getDecorationAngle(),
- scale: getDecorationScale(),
- }"
- alt=""
- >
+ <template v-if="showDecoration">
+ <img
+ v-for="decoration in decorations ?? user.avatarDecorations"
+ :class="[$style.decoration]"
+ :src="decoration.url"
+ :style="{
+ rotate: getDecorationAngle(decoration),
+ scale: getDecorationScale(decoration),
+ translate: getDecorationOffset(decoration),
+ }"
+ alt=""
+ >
+ </template>
</component>
</template>
<script lang="ts" setup>
-import { watch } from 'vue';
+import { watch, ref, computed } from 'vue';
import * as Misskey from 'misskey-js';
import MkImgWithBlurhash from '../MkImgWithBlurhash.vue';
import MkA from './MkA.vue';
@@ -47,9 +50,9 @@ import { acct, userPage } from '@/filters/user.js';
import MkUserOnlineIndicator from '@/components/MkUserOnlineIndicator.vue';
import { defaultStore } from '@/store.js';
-const animation = $ref(defaultStore.state.animation);
-const squareAvatars = $ref(defaultStore.state.squareAvatars);
-const useBlurEffect = $ref(defaultStore.state.useBlurEffect);
+const animation = ref(defaultStore.state.animation);
+const squareAvatars = ref(defaultStore.state.squareAvatars);
+const useBlurEffect = ref(defaultStore.state.useBlurEffect);
const props = withDefaults(defineProps<{
user: Misskey.entities.User;
@@ -57,19 +60,14 @@ const props = withDefaults(defineProps<{
link?: boolean;
preview?: boolean;
indicator?: boolean;
- decoration?: {
- url: string;
- angle?: number;
- flipH?: boolean;
- flipV?: boolean;
- };
+ decorations?: Omit<Misskey.entities.UserDetailed['avatarDecorations'][number], 'id'>[];
forceShowDecoration?: boolean;
}>(), {
target: null,
link: false,
preview: false,
indicator: false,
- decoration: undefined,
+ decorations: undefined,
forceShowDecoration: false,
});
@@ -79,11 +77,11 @@ const emit = defineEmits<{
const showDecoration = props.forceShowDecoration || defaultStore.state.showAvatarDecorations;
-const bound = $computed(() => props.link
+const bound = computed(() => props.link
? { to: userPage(props.user), target: props.target }
: {});
-const url = $computed(() => (defaultStore.state.disableShowingAnimatedImages || defaultStore.state.enableDataSaverMode)
+const url = computed(() => (defaultStore.state.disableShowingAnimatedImages || defaultStore.state.dataSaver.avatar)
? getStaticImageUrl(props.user.avatarUrl)
: props.user.avatarUrl);
@@ -92,34 +90,26 @@ function onClick(ev: MouseEvent): void {
emit('click', ev);
}
-function getDecorationAngle() {
- let angle;
- if (props.decoration) {
- angle = props.decoration.angle ?? 0;
- } else if (props.user.avatarDecorations.length > 0) {
- angle = props.user.avatarDecorations[0].angle ?? 0;
- } else {
- angle = 0;
- }
+function getDecorationAngle(decoration: Omit<Misskey.entities.UserDetailed['avatarDecorations'][number], 'id'>) {
+ const angle = decoration.angle ?? 0;
return angle === 0 ? undefined : `${angle * 360}deg`;
}
-function getDecorationScale() {
- let scaleX;
- if (props.decoration) {
- scaleX = props.decoration.flipH ? -1 : 1;
- } else if (props.user.avatarDecorations.length > 0) {
- scaleX = props.user.avatarDecorations[0].flipH ? -1 : 1;
- } else {
- scaleX = 1;
- }
+function getDecorationScale(decoration: Omit<Misskey.entities.UserDetailed['avatarDecorations'][number], 'id'>) {
+ const scaleX = decoration.flipH ? -1 : 1;
return scaleX === 1 ? undefined : `${scaleX} 1`;
}
-let color = $ref<string | undefined>();
+function getDecorationOffset(decoration: Omit<Misskey.entities.UserDetailed['avatarDecorations'][number], 'id'>) {
+ const offsetX = decoration.offsetX ?? 0;
+ const offsetY = decoration.offsetY ?? 0;
+ return offsetX === 0 && offsetY === 0 ? undefined : `${offsetX * 100}% ${offsetY * 100}%`;
+}
+
+const color = ref<string | undefined>();
watch(() => props.user.avatarBlurhash, () => {
- color = extractAvgColorFromBlurhash(props.user.avatarBlurhash);
+ color.value = extractAvgColorFromBlurhash(props.user.avatarBlurhash);
}, {
immediate: true,
});
diff --git a/packages/frontend/src/components/global/MkCustomEmoji.vue b/packages/frontend/src/components/global/MkCustomEmoji.vue
index 10d7d93b01..e8732d1b16 100644
--- a/packages/frontend/src/components/global/MkCustomEmoji.vue
+++ b/packages/frontend/src/components/global/MkCustomEmoji.vue
@@ -19,12 +19,13 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { computed, inject } from 'vue';
+import { computed, inject, ref } from 'vue';
import { getProxiedImageUrl, getStaticImageUrl } from '@/scripts/media-proxy.js';
import { defaultStore } from '@/store.js';
import { customEmojisMap } from '@/custom-emojis.js';
import * as os from '@/os.js';
import copyToClipboard from '@/scripts/copy-to-clipboard.js';
+import * as sound from '@/scripts/sound.js';
import { i18n } from '@/i18n.js';
const props = defineProps<{
@@ -71,7 +72,7 @@ const url = computed(() => {
});
const alt = computed(() => `:${customEmojiName.value}:`);
-let errored = $ref(url.value == null);
+const errored = ref(url.value == null);
function onClick(ev: MouseEvent) {
if (props.menu) {
@@ -90,6 +91,7 @@ function onClick(ev: MouseEvent) {
icon: 'ph-smiley ph-bold ph-lg',
action: () => {
react(`:${props.name}:`);
+ sound.play('reaction');
},
}] : [])], ev.currentTarget ?? ev.target);
}
diff --git a/packages/frontend/src/components/global/MkEmoji.vue b/packages/frontend/src/components/global/MkEmoji.vue
index d5025edf82..b1d62db33c 100644
--- a/packages/frontend/src/components/global/MkEmoji.vue
+++ b/packages/frontend/src/components/global/MkEmoji.vue
@@ -16,6 +16,7 @@ import { defaultStore } from '@/store.js';
import { getEmojiName } from '@/scripts/emojilist.js';
import * as os from '@/os.js';
import copyToClipboard from '@/scripts/copy-to-clipboard.js';
+import * as sound from '@/scripts/sound.js';
import { i18n } from '@/i18n.js';
const props = defineProps<{
@@ -56,6 +57,7 @@ function onClick(ev: MouseEvent) {
icon: 'ph-smiley ph-bold ph-lg',
action: () => {
react(props.emoji);
+ sound.play('reaction');
},
}] : [])], ev.currentTarget ?? ev.target);
}
diff --git a/packages/frontend/src/components/global/MkLazy.vue b/packages/frontend/src/components/global/MkLazy.vue
new file mode 100644
index 0000000000..6d7ff4ca49
--- /dev/null
+++ b/packages/frontend/src/components/global/MkLazy.vue
@@ -0,0 +1,53 @@
+<!--
+SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-License-Identifier: AGPL-3.0-only
+-->
+
+<template>
+<div ref="rootEl" :class="$style.root">
+ <div v-if="!showing" :class="$style.placeholder"></div>
+ <slot v-else></slot>
+</div>
+</template>
+
+<script lang="ts" setup>
+import { nextTick, onMounted, onActivated, onBeforeUnmount, ref, shallowRef } from 'vue';
+
+const rootEl = shallowRef<HTMLDivElement>();
+const showing = ref(false);
+
+const observer = new IntersectionObserver(
+ (entries) => {
+ if (entries.some((entry) => entry.isIntersecting)) {
+ showing.value = true;
+ }
+ },
+);
+
+onMounted(() => {
+ nextTick(() => {
+ observer.observe(rootEl.value!);
+ });
+});
+
+onActivated(() => {
+ nextTick(() => {
+ observer.observe(rootEl.value!);
+ });
+});
+
+onBeforeUnmount(() => {
+ observer.disconnect();
+});
+</script>
+
+<style lang="scss" module>
+.root {
+ display: block;
+}
+
+.placeholder {
+ display: block;
+ min-height: 150px;
+}
+</style>
diff --git a/packages/frontend/src/components/global/MkMisskeyFlavoredMarkdown.ts b/packages/frontend/src/components/global/MkMisskeyFlavoredMarkdown.ts
index bd6a599a98..60d12fdcde 100644
--- a/packages/frontend/src/components/global/MkMisskeyFlavoredMarkdown.ts
+++ b/packages/frontend/src/components/global/MkMisskeyFlavoredMarkdown.ts
@@ -37,7 +37,7 @@ type MfmProps = {
isNote?: boolean;
emojiUrls?: string[];
rootScale?: number;
- nyaize: boolean | 'respect';
+ nyaize?: boolean | 'respect';
parsedNodes?: mfm.MfmNode[] | null;
enableEmojiMenu?: boolean;
enableEmojiMenuReaction?: boolean;
@@ -110,26 +110,30 @@ export default function(props: MfmProps) {
case 'fn': {
// TODO: CSSを文字列で組み立てていくと token.props.args.~~~ 経由でCSSインジェクションできるのでよしなにやる
- let style;
+ let style: string | undefined;
switch (token.props.name) {
case 'tada': {
const speed = validTime(token.props.args.speed) ?? '1s';
- style = 'font-size: 150%;' + (useAnim ? `animation: tada ${speed} linear infinite both;` : '');
+ const delay = validTime(token.props.args.delay) ?? '0s';
+ style = 'font-size: 150%;' + (useAnim ? `animation: tada ${speed} linear infinite both; animation-delay: ${delay};` : '');
break;
}
case 'jelly': {
const speed = validTime(token.props.args.speed) ?? '1s';
- style = (useAnim ? `animation: mfm-rubberBand ${speed} linear infinite both;` : '');
+ const delay = validTime(token.props.args.delay) ?? '0s';
+ style = (useAnim ? `animation: mfm-rubberBand ${speed} linear infinite both; animation-delay: ${delay};` : '');
break;
}
case 'twitch': {
const speed = validTime(token.props.args.speed) ?? '0.5s';
- style = useAnim ? `animation: mfm-twitch ${speed} ease infinite;` : '';
+ const delay = validTime(token.props.args.delay) ?? '0s';
+ style = useAnim ? `animation: mfm-twitch ${speed} ease infinite; animation-delay: ${delay};` : '';
break;
}
case 'shake': {
const speed = validTime(token.props.args.speed) ?? '0.5s';
- style = useAnim ? `animation: mfm-shake ${speed} ease infinite;` : '';
+ const delay = validTime(token.props.args.delay) ?? '0s';
+ style = useAnim ? `animation: mfm-shake ${speed} ease infinite; animation-delay: ${delay};` : '';
break;
}
case 'spin': {
@@ -142,17 +146,20 @@ export default function(props: MfmProps) {
token.props.args.y ? 'mfm-spinY' :
'mfm-spin';
const speed = validTime(token.props.args.speed) ?? '1.5s';
- style = useAnim ? `animation: ${anime} ${speed} linear infinite; animation-direction: ${direction};` : '';
+ const delay = validTime(token.props.args.delay) ?? '0s';
+ style = useAnim ? `animation: ${anime} ${speed} linear infinite; animation-direction: ${direction}; animation-delay: ${delay};` : '';
break;
}
case 'jump': {
const speed = validTime(token.props.args.speed) ?? '0.75s';
- style = useAnim ? `animation: mfm-jump ${speed} linear infinite;` : '';
+ const delay = validTime(token.props.args.delay) ?? '0s';
+ style = useAnim ? `animation: mfm-jump ${speed} linear infinite; animation-delay: ${delay};` : '';
break;
}
case 'bounce': {
const speed = validTime(token.props.args.speed) ?? '0.75s';
- style = useAnim ? `animation: mfm-bounce ${speed} linear infinite; transform-origin: center bottom;` : '';
+ const delay = validTime(token.props.args.delay) ?? '0s';
+ style = useAnim ? `animation: mfm-bounce ${speed} linear infinite; transform-origin: center bottom; animation-delay: ${delay};` : '';
break;
}
case 'flip': {
@@ -202,7 +209,8 @@ export default function(props: MfmProps) {
}, genEl(token.children, scale));
}
const speed = validTime(token.props.args.speed) ?? '1s';
- style = `animation: mfm-rainbow ${speed} linear infinite;`;
+ const delay = validTime(token.props.args.delay) ?? '0s';
+ style = `animation: mfm-rainbow ${speed} linear infinite; animation-delay: ${delay};`;
break;
}
case 'sparkle': {
@@ -249,11 +257,17 @@ export default function(props: MfmProps) {
case 'ruby': {
if (token.children.length === 1) {
const child = token.children[0];
- const text = child.type === 'text' ? child.props.text : '';
+ let text = child.type === 'text' ? child.props.text : '';
+ if (!disableNyaize && shouldNyaize) {
+ text = doNyaize(text);
+ }
return h('ruby', {}, [text.split(' ')[0], h('rt', text.split(' ')[1])]);
} else {
const rt = token.children.at(-1)!;
- const text = rt.type === 'text' ? rt.props.text : '';
+ let text = rt.type === 'text' ? rt.props.text : '';
+ if (!disableNyaize && shouldNyaize) {
+ text = doNyaize(text);
+ }
return h('ruby', {}, [...genEl(token.children.slice(0, token.children.length - 1), scale), h('rt', text.trim())]);
}
}
@@ -275,7 +289,7 @@ export default function(props: MfmProps) {
]);
}
}
- if (style == null) {
+ if (style === undefined) {
return h('span', {}, ['$[', token.props.name, ' ', ...genEl(token.children, scale), ']']);
} else {
return h('span', {
diff --git a/packages/frontend/src/components/global/MkPageHeader.vue b/packages/frontend/src/components/global/MkPageHeader.vue
index fd7aec5e5a..a36d9517cd 100644
--- a/packages/frontend/src/components/global/MkPageHeader.vue
+++ b/packages/frontend/src/components/global/MkPageHeader.vue
@@ -50,23 +50,19 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { onMounted, onUnmounted, ref, inject } from 'vue';
+import { onMounted, onUnmounted, ref, inject, shallowRef, computed } from 'vue';
import tinycolor from 'tinycolor2';
import XTabs, { Tab } from './MkPageHeader.tabs.vue';
import { scrollToTop } from '@/scripts/scroll.js';
import { globalEvents } from '@/events.js';
import { injectPageMetadata } from '@/scripts/page-metadata.js';
import { $i, openAccountMenu as openAccountMenu_ } from '@/account.js';
+import { PageHeaderItem } from '@/types/page-header.js';
const props = withDefaults(defineProps<{
tabs?: Tab[];
tab?: string;
- actions?: {
- text: string;
- icon: string;
- highlighted?: boolean;
- handler: (ev: MouseEvent) => void;
- }[];
+ actions?: PageHeaderItem[] | null;
thin?: boolean;
displayMyAvatar?: boolean;
displayBackButton?: boolean;
@@ -85,13 +81,13 @@ const metadata = injectPageMetadata();
const hideTitle = inject('shouldOmitHeaderTitle', false);
const thin_ = props.thin || inject('shouldHeaderThin', false);
-let el = $shallowRef<HTMLElement | undefined>(undefined);
+const el = shallowRef<HTMLElement | undefined>(undefined);
const bg = ref<string | undefined>(undefined);
-let narrow = $ref(false);
-const hasTabs = $computed(() => props.tabs.length > 0);
-const hasActions = $computed(() => props.actions && props.actions.length > 0);
-const show = $computed(() => {
- return !hideTitle || hasTabs || hasActions;
+const narrow = ref(false);
+const hasTabs = computed(() => props.tabs.length > 0);
+const hasActions = computed(() => props.actions && props.actions.length > 0);
+const show = computed(() => {
+ return !hideTitle || hasTabs.value || hasActions.value;
});
const preventDrag = (ev: TouchEvent) => {
@@ -99,8 +95,8 @@ const preventDrag = (ev: TouchEvent) => {
};
const top = () => {
- if (el) {
- scrollToTop(el as HTMLElement, { behavior: 'smooth' });
+ if (el.value) {
+ scrollToTop(el.value as HTMLElement, { behavior: 'smooth' });
}
};
@@ -131,14 +127,14 @@ onMounted(() => {
calcBg();
globalEvents.on('themeChanged', calcBg);
- if (el && el.parentElement) {
- narrow = el.parentElement.offsetWidth < 500;
+ if (el.value && el.value.parentElement) {
+ narrow.value = el.value.parentElement.offsetWidth < 500;
ro = new ResizeObserver((entries, observer) => {
- if (el && el.parentElement && document.body.contains(el as HTMLElement)) {
- narrow = el.parentElement.offsetWidth < 500;
+ if (el.value && el.value.parentElement && document.body.contains(el.value as HTMLElement)) {
+ narrow.value = el.value.parentElement.offsetWidth < 500;
}
});
- ro.observe(el.parentElement as HTMLElement);
+ ro.observe(el.value.parentElement as HTMLElement);
}
});
diff --git a/packages/frontend/src/components/global/MkStickyContainer.vue b/packages/frontend/src/components/global/MkStickyContainer.vue
index 8e9bff11d1..1d707af2d1 100644
--- a/packages/frontend/src/components/global/MkStickyContainer.vue
+++ b/packages/frontend/src/components/global/MkStickyContainer.vue
@@ -18,36 +18,36 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { onMounted, onUnmounted, provide, inject, Ref, ref, watch } from 'vue';
-import { $$ } from 'vue/macros';
+import { onMounted, onUnmounted, provide, inject, Ref, ref, watch, shallowRef } from 'vue';
+
import { CURRENT_STICKY_BOTTOM, CURRENT_STICKY_TOP } from '@/const';
-const rootEl = $shallowRef<HTMLElement>();
-const headerEl = $shallowRef<HTMLElement>();
-const footerEl = $shallowRef<HTMLElement>();
-const bodyEl = $shallowRef<HTMLElement>();
+const rootEl = shallowRef<HTMLElement>();
+const headerEl = shallowRef<HTMLElement>();
+const footerEl = shallowRef<HTMLElement>();
+const bodyEl = shallowRef<HTMLElement>();
-let headerHeight = $ref<string | undefined>();
-let childStickyTop = $ref(0);
+const headerHeight = ref<string | undefined>();
+const childStickyTop = ref(0);
const parentStickyTop = inject<Ref<number>>(CURRENT_STICKY_TOP, ref(0));
-provide(CURRENT_STICKY_TOP, $$(childStickyTop));
+provide(CURRENT_STICKY_TOP, childStickyTop);
-let footerHeight = $ref<string | undefined>();
-let childStickyBottom = $ref(0);
+const footerHeight = ref<string | undefined>();
+const childStickyBottom = ref(0);
const parentStickyBottom = inject<Ref<number>>(CURRENT_STICKY_BOTTOM, ref(0));
-provide(CURRENT_STICKY_BOTTOM, $$(childStickyBottom));
+provide(CURRENT_STICKY_BOTTOM, childStickyBottom);
const calc = () => {
// コンポーネントが表示されてないけどKeepAliveで残ってる場合などは null になる
- if (headerEl != null) {
- childStickyTop = parentStickyTop.value + headerEl.offsetHeight;
- headerHeight = headerEl.offsetHeight.toString();
+ if (headerEl.value != null) {
+ childStickyTop.value = parentStickyTop.value + headerEl.value.offsetHeight;
+ headerHeight.value = headerEl.value.offsetHeight.toString();
}
// コンポーネントが表示されてないけどKeepAliveで残ってる場合などは null になる
- if (footerEl != null) {
- childStickyBottom = parentStickyBottom.value + footerEl.offsetHeight;
- footerHeight = footerEl.offsetHeight.toString();
+ if (footerEl.value != null) {
+ childStickyBottom.value = parentStickyBottom.value + footerEl.value.offsetHeight;
+ footerHeight.value = footerEl.value.offsetHeight.toString();
}
};
@@ -62,28 +62,28 @@ onMounted(() => {
watch([parentStickyTop, parentStickyBottom], calc);
- watch($$(childStickyTop), () => {
- bodyEl.style.setProperty('--stickyTop', `${childStickyTop}px`);
+ watch(childStickyTop, () => {
+ bodyEl.value.style.setProperty('--stickyTop', `${childStickyTop.value}px`);
}, {
immediate: true,
});
- watch($$(childStickyBottom), () => {
- bodyEl.style.setProperty('--stickyBottom', `${childStickyBottom}px`);
+ watch(childStickyBottom, () => {
+ bodyEl.value.style.setProperty('--stickyBottom', `${childStickyBottom.value}px`);
}, {
immediate: true,
});
- headerEl.style.position = 'sticky';
- headerEl.style.top = 'var(--stickyTop, 0)';
- headerEl.style.zIndex = '1000';
+ headerEl.value.style.position = 'sticky';
+ headerEl.value.style.top = 'var(--stickyTop, 0)';
+ headerEl.value.style.zIndex = '1000';
- footerEl.style.position = 'sticky';
- footerEl.style.bottom = 'var(--stickyBottom, 0)';
- footerEl.style.zIndex = '1000';
+ footerEl.value.style.position = 'sticky';
+ footerEl.value.style.bottom = 'var(--stickyBottom, 0)';
+ footerEl.value.style.zIndex = '1000';
- observer.observe(headerEl);
- observer.observe(footerEl);
+ observer.observe(headerEl.value);
+ observer.observe(footerEl.value);
});
onUnmounted(() => {
@@ -91,6 +91,6 @@ onUnmounted(() => {
});
defineExpose({
- rootEl: $$(rootEl),
+ rootEl: rootEl,
});
</script>
diff --git a/packages/frontend/src/components/global/MkTime.vue b/packages/frontend/src/components/global/MkTime.vue
index f08d538fc0..e11db9dc31 100644
--- a/packages/frontend/src/components/global/MkTime.vue
+++ b/packages/frontend/src/components/global/MkTime.vue
@@ -14,7 +14,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import isChromatic from 'chromatic/isChromatic';
-import { onMounted, onUnmounted } from 'vue';
+import { onMounted, onUnmounted, ref, computed } from 'vue';
import { i18n } from '@/i18n.js';
import { dateTimeFormat } from '@/scripts/intl-const.js';
@@ -28,35 +28,48 @@ const props = withDefaults(defineProps<{
mode: 'relative',
});
-const _time = props.time == null ? NaN :
- typeof props.time === 'number' ? props.time :
- (props.time instanceof Date ? props.time : new Date(props.time)).getTime();
+function getDateSafe(n: Date | string | number) {
+ try {
+ if (n instanceof Date) {
+ return n;
+ }
+ return new Date(n);
+ } catch (err) {
+ return {
+ getTime: () => NaN,
+ };
+ }
+}
+
+// eslint-disable-next-line vue/no-setup-props-destructure
+const _time = props.time == null ? NaN : getDateSafe(props.time).getTime();
const invalid = Number.isNaN(_time);
const absolute = !invalid ? dateTimeFormat.format(_time) : i18n.ts._ago.invalid;
-let now = $ref((props.origin ?? new Date()).getTime());
-const ago = $computed(() => (now - _time) / 1000/*ms*/);
+// eslint-disable-next-line vue/no-setup-props-destructure
+const now = ref((props.origin ?? new Date()).getTime());
+const ago = computed(() => (now.value - _time) / 1000/*ms*/);
-const relative = $computed<string>(() => {
+const relative = computed<string>(() => {
if (props.mode === 'absolute') return ''; // absoluteではrelativeを使わないので計算しない
if (invalid) return i18n.ts._ago.invalid;
return (
- ago >= 31536000 ? i18n.t('_ago.yearsAgo', { n: Math.round(ago / 31536000).toString() }) :
- ago >= 2592000 ? i18n.t('_ago.monthsAgo', { n: Math.round(ago / 2592000).toString() }) :
- ago >= 604800 ? i18n.t('_ago.weeksAgo', { n: Math.round(ago / 604800).toString() }) :
- ago >= 86400 ? i18n.t('_ago.daysAgo', { n: Math.round(ago / 86400).toString() }) :
- ago >= 3600 ? i18n.t('_ago.hoursAgo', { n: Math.round(ago / 3600).toString() }) :
- ago >= 60 ? i18n.t('_ago.minutesAgo', { n: (~~(ago / 60)).toString() }) :
- ago >= 10 ? i18n.t('_ago.secondsAgo', { n: (~~(ago % 60)).toString() }) :
- ago >= -3 ? i18n.ts._ago.justNow :
- ago < -31536000 ? i18n.t('_timeIn.years', { n: Math.round(-ago / 31536000).toString() }) :
- ago < -2592000 ? i18n.t('_timeIn.months', { n: Math.round(-ago / 2592000).toString() }) :
- ago < -604800 ? i18n.t('_timeIn.weeks', { n: Math.round(-ago / 604800).toString() }) :
- ago < -86400 ? i18n.t('_timeIn.days', { n: Math.round(-ago / 86400).toString() }) :
- ago < -3600 ? i18n.t('_timeIn.hours', { n: Math.round(-ago / 3600).toString() }) :
- ago < -60 ? i18n.t('_timeIn.minutes', { n: (~~(-ago / 60)).toString() }) :
- i18n.t('_timeIn.seconds', { n: (~~(-ago % 60)).toString() })
+ ago.value >= 31536000 ? i18n.t('_ago.yearsAgo', { n: Math.round(ago.value / 31536000).toString() }) :
+ ago.value >= 2592000 ? i18n.t('_ago.monthsAgo', { n: Math.round(ago.value / 2592000).toString() }) :
+ ago.value >= 604800 ? i18n.t('_ago.weeksAgo', { n: Math.round(ago.value / 604800).toString() }) :
+ ago.value >= 86400 ? i18n.t('_ago.daysAgo', { n: Math.round(ago.value / 86400).toString() }) :
+ ago.value >= 3600 ? i18n.t('_ago.hoursAgo', { n: Math.round(ago.value / 3600).toString() }) :
+ ago.value >= 60 ? i18n.t('_ago.minutesAgo', { n: (~~(ago.value / 60)).toString() }) :
+ ago.value >= 10 ? i18n.t('_ago.secondsAgo', { n: (~~(ago.value % 60)).toString() }) :
+ ago.value >= -3 ? i18n.ts._ago.justNow :
+ ago.value < -31536000 ? i18n.t('_timeIn.years', { n: Math.round(-ago.value / 31536000).toString() }) :
+ ago.value < -2592000 ? i18n.t('_timeIn.months', { n: Math.round(-ago.value / 2592000).toString() }) :
+ ago.value < -604800 ? i18n.t('_timeIn.weeks', { n: Math.round(-ago.value / 604800).toString() }) :
+ ago.value < -86400 ? i18n.t('_timeIn.days', { n: Math.round(-ago.value / 86400).toString() }) :
+ ago.value < -3600 ? i18n.t('_timeIn.hours', { n: Math.round(-ago.value / 3600).toString() }) :
+ ago.value < -60 ? i18n.t('_timeIn.minutes', { n: (~~(-ago.value / 60)).toString() }) :
+ i18n.t('_timeIn.seconds', { n: (~~(-ago.value % 60)).toString() })
);
});
@@ -64,8 +77,8 @@ let tickId: number;
let currentInterval: number;
function tick() {
- now = (new Date()).getTime();
- const nextInterval = ago < 60 ? 10000 : ago < 3600 ? 60000 : 180000;
+ now.value = (new Date()).getTime();
+ const nextInterval = ago.value < 60 ? 10000 : ago.value < 3600 ? 60000 : 180000;
if (currentInterval !== nextInterval) {
if (tickId) window.clearInterval(tickId);
diff --git a/packages/frontend/src/components/global/MkUrl.vue b/packages/frontend/src/components/global/MkUrl.vue
index d29c720278..667a113432 100644
--- a/packages/frontend/src/components/global/MkUrl.vue
+++ b/packages/frontend/src/components/global/MkUrl.vue
@@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template>
<component
- :is="self ? 'MkA' : 'a'" ref="el" :class="$style.root" class="_link" :[attr]="self ? props.url.substring(local.length) : props.url" :rel="rel" :target="target"
+ :is="self ? 'MkA' : 'a'" ref="el" :class="$style.root" class="_link" :[attr]="self ? props.url.substring(local.length) : props.url" :rel="rel ?? 'nofollow noopener'" :target="target"
@contextmenu.stop="() => {}"
>
<template v-if="!self">
diff --git a/packages/frontend/src/components/global/MkUserName.stories.impl.ts b/packages/frontend/src/components/global/MkUserName.stories.impl.ts
index 8c24a4819f..01455e492d 100644
--- a/packages/frontend/src/components/global/MkUserName.stories.impl.ts
+++ b/packages/frontend/src/components/global/MkUserName.stories.impl.ts
@@ -5,7 +5,6 @@
/* eslint-disable @typescript-eslint/explicit-function-return-type */
import { expect } from '@storybook/jest';
-import { userEvent, within } from '@storybook/testing-library';
import { StoryObj } from '@storybook/vue3';
import { userDetailed } from '../../../.storybook/fakes';
import MkUserName from './MkUserName.vue';
diff --git a/packages/frontend/src/components/global/RouterView.vue b/packages/frontend/src/components/global/RouterView.vue
index 99f42f4fcb..9da8f8c379 100644
--- a/packages/frontend/src/components/global/RouterView.vue
+++ b/packages/frontend/src/components/global/RouterView.vue
@@ -16,7 +16,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { inject, onBeforeUnmount, provide } from 'vue';
+import { inject, onBeforeUnmount, provide, shallowRef, ref } from 'vue';
import { Resolved, Router } from '@/nirax';
import { defaultStore } from '@/store.js';
@@ -46,16 +46,16 @@ function resolveNested(current: Resolved, d = 0): Resolved | null {
}
const current = resolveNested(router.current)!;
-let currentPageComponent = $shallowRef(current.route.component);
-let currentPageProps = $ref(current.props);
-let key = $ref(current.route.path + JSON.stringify(Object.fromEntries(current.props)));
+const currentPageComponent = shallowRef(current.route.component);
+const currentPageProps = ref(current.props);
+const key = ref(current.route.path + JSON.stringify(Object.fromEntries(current.props)));
function onChange({ resolved, key: newKey }) {
const current = resolveNested(resolved);
if (current == null) return;
- currentPageComponent = current.route.component;
- currentPageProps = current.props;
- key = current.route.path + JSON.stringify(Object.fromEntries(current.props));
+ currentPageComponent.value = current.route.component;
+ currentPageProps.value = current.props;
+ key.value = current.route.path + JSON.stringify(Object.fromEntries(current.props));
}
router.addListener('change', onChange);
diff --git a/packages/frontend/src/components/index.ts b/packages/frontend/src/components/index.ts
index c740d181f9..a3e13c3a50 100644
--- a/packages/frontend/src/components/index.ts
+++ b/packages/frontend/src/components/index.ts
@@ -25,6 +25,7 @@ import MkPageHeader from './global/MkPageHeader.vue';
import MkSpacer from './global/MkSpacer.vue';
import MkFooterSpacer from './global/MkFooterSpacer.vue';
import MkStickyContainer from './global/MkStickyContainer.vue';
+import MkLazy from './global/MkLazy.vue';
export default function(app: App) {
for (const [key, value] of Object.entries(components)) {
@@ -53,6 +54,7 @@ export const components = {
MkSpacer: MkSpacer,
MkFooterSpacer: MkFooterSpacer,
MkStickyContainer: MkStickyContainer,
+ MkLazy: MkLazy,
};
declare module '@vue/runtime-core' {
@@ -77,5 +79,6 @@ declare module '@vue/runtime-core' {
MkSpacer: typeof MkSpacer;
MkFooterSpacer: typeof MkFooterSpacer;
MkStickyContainer: typeof MkStickyContainer;
+ MkLazy: typeof MkLazy;
}
}
diff --git a/packages/frontend/src/components/page/page.text.vue b/packages/frontend/src/components/page/page.text.vue
index 6aa2c1c0b7..892522d4b5 100644
--- a/packages/frontend/src/components/page/page.text.vue
+++ b/packages/frontend/src/components/page/page.text.vue
@@ -16,7 +16,6 @@ import * as mfm from '@sharkey/sfm-js';
import * as Misskey from 'misskey-js';
import { TextBlock } from './block.type';
import { extractUrlFromMfm } from '@/scripts/extract-url-from-mfm.js';
-import { $i } from '@/account.js';
const MkUrlPreview = defineAsyncComponent(() => import('@/components/MkUrlPreview.vue'));
diff --git a/packages/frontend/src/components/page/page.vue b/packages/frontend/src/components/page/page.vue
index ab37ca69ad..94ca7bdf04 100644
--- a/packages/frontend/src/components/page/page.vue
+++ b/packages/frontend/src/components/page/page.vue
@@ -10,7 +10,6 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { onMounted, nextTick } from 'vue';
import * as Misskey from 'misskey-js';
import XBlock from './page.block.vue';
diff --git a/packages/frontend/src/const.ts b/packages/frontend/src/const.ts
index 2f8d57c7f6..cdd6731269 100644
--- a/packages/frontend/src/const.ts
+++ b/packages/frontend/src/const.ts
@@ -105,7 +105,21 @@ https://github.com/sindresorhus/file-type/blob/main/core.js
https://developer.mozilla.org/en-US/docs/Web/Media/Formats/Containers
*/
-export const notificationTypes = ['note', 'follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollEnded', 'receiveFollowRequest', 'followRequestAccepted', 'achievementEarned', 'app'] as const;
+export const notificationTypes = [
+ 'note',
+ 'follow',
+ 'mention',
+ 'reply',
+ 'renote',
+ 'quote',
+ 'reaction',
+ 'pollEnded',
+ 'receiveFollowRequest',
+ 'followRequestAccepted',
+ 'roleAssigned',
+ 'achievementEarned',
+ 'app',
+] as const;
export const obsoleteNotificationTypes = ['pollVote', 'groupInvited'] as const;
export const ROLE_POLICIES = [
@@ -134,6 +148,7 @@ export const ROLE_POLICIES = [
'userListLimit',
'userEachUserListsLimit',
'rateLimitFactor',
+ 'avatarDecorationLimit',
] as const;
// なんか動かない
diff --git a/packages/frontend/src/custom-emojis.ts b/packages/frontend/src/custom-emojis.ts
index 8ecd1bd2eb..6a48159f13 100644
--- a/packages/frontend/src/custom-emojis.ts
+++ b/packages/frontend/src/custom-emojis.ts
@@ -10,7 +10,7 @@ import { useStream } from '@/stream.js';
import { get, set } from '@/scripts/idb-proxy.js';
const storageCache = await get('emojis');
-export const customEmojis = shallowRef<Misskey.entities.CustomEmoji[]>(Array.isArray(storageCache) ? storageCache : []);
+export const customEmojis = shallowRef<Misskey.entities.EmojiSimple[]>(Array.isArray(storageCache) ? storageCache : []);
export const customEmojiCategories = computed<[ ...string[], null ]>(() => {
const categories = new Set<string>();
for (const emoji of customEmojis.value) {
@@ -21,7 +21,7 @@ export const customEmojiCategories = computed<[ ...string[], null ]>(() => {
return markRaw([...Array.from(categories), null]);
});
-export const customEmojisMap = new Map<string, Misskey.entities.CustomEmoji>();
+export const customEmojisMap = new Map<string, Misskey.entities.EmojiSimple>();
watch(customEmojis, emojis => {
customEmojisMap.clear();
for (const emoji of emojis) {
@@ -38,7 +38,7 @@ stream.on('emojiAdded', emojiData => {
});
stream.on('emojiUpdated', emojiData => {
- customEmojis.value = customEmojis.value.map(item => emojiData.emojis.find(search => search.name === item.name) as Misskey.entities.CustomEmoji ?? item);
+ customEmojis.value = customEmojis.value.map(item => emojiData.emojis.find(search => search.name === item.name) as Misskey.entities.EmojiSimple ?? item);
set('emojis', customEmojis.value);
});
diff --git a/packages/frontend/src/emojilist.json b/packages/frontend/src/emojilist.json
index fe1d884ebe..75d5c34d71 100644
--- a/packages/frontend/src/emojilist.json
+++ b/packages/frontend/src/emojilist.json
@@ -103,6 +103,7 @@
["🫥", "dotted_line_face", 0],
["🫤", "face_with_diagonal_mouth", 0],
["🥹", "face_holding_back_tears", 0],
+ ["🫨", "shaking_face", 0],
["💩", "poop", 0],
["😈", "smiling_imp", 0],
["👿", "imp", 0],
@@ -132,6 +133,8 @@
["✊", "fist", 1],
["🤛", "fist_left", 1],
["🤜", "fist_right", 1],
+ ["🫷", "leftwards_pushing_hand", 1],
+ ["🫸", "rightwards_pushing_hand", 1],
["✌", "v", 1],
["👌", "ok_hand", 1],
["✋", "raised_hand", 1],
@@ -453,6 +456,7 @@
["🐸", "frog", 2],
["🦑", "squid", 2],
["🐙", "octopus", 2],
+ ["🪼", "jellyfish", 2],
["🦐", "shrimp", 2],
["🐵", "monkey_face", 2],
["🦍", "gorilla", 2],
@@ -466,7 +470,9 @@
["🐤", "baby_chick", 2],
["🐣", "hatching_chick", 2],
["🐥", "hatched_chick", 2],
+ ["🪿", "goose", 2],
["🦆", "duck", 2],
+ ["🐦‍⬛", "black_bird", 2],
["🦅", "eagle", 2],
["🦉", "owl", 2],
["🦇", "bat", 2],
@@ -474,6 +480,7 @@
["🐗", "boar", 2],
["🐴", "horse", 2],
["🦄", "unicorn", 2],
+ ["🫎", "moose", 2],
["🐝", "honeybee", 2],
["🐛", "bug", 2],
["🦋", "butterfly", 2],
@@ -516,6 +523,7 @@
["🐐", "goat", 2],
["🐏", "ram", 2],
["🐑", "sheep", 2],
+ ["🫏", "donkey", 2],
["🐎", "racehorse", 2],
["🐖", "pig2", 2],
["🐀", "rat", 2],
@@ -546,6 +554,7 @@
["🐻‍❄️", "polar_bear", 2],
["🦤", "dodo", 2],
["🪶", "feather", 2],
+ ["🪽", "wing", 2],
["🦭", "seal", 2],
["🐾", "paw_prints", 2],
["🐉", "dragon", 2],
@@ -576,6 +585,7 @@
["🌻", "sunflower", 2],
["🌹", "rose", 2],
["🥀", "wilted_flower", 2],
+ ["🪻", "hyacinth", 2],
["🌷", "tulip", 2],
["🌼", "blossom", 2],
["🌸", "cherry_blossom", 2],
@@ -655,6 +665,7 @@
["🥝", "kiwi_fruit", 3],
["🥭", "mango", 3],
["🥑", "avocado", 3],
+ ["🫛", "pea_pod", 3],
["🥦", "broccoli", 3],
["🍅", "tomato", 3],
["🍆", "eggplant", 3],
@@ -668,6 +679,7 @@
["🌽", "corn", 3],
["🥬", "leafy_greens", 3],
["🍠", "sweet_potato", 3],
+ ["🫚", "ginger_root", 3],
["🥜", "peanuts", 3],
["🧄", "garlic", 3],
["🧅", "onion", 3],
@@ -850,9 +862,11 @@
["🎧", "headphones", 4],
["🎼", "musical_score", 4],
["🎹", "musical_keyboard", 4],
+ ["🪇", "maracas", 4],
["🥁", "drum", 4],
["🎷", "saxophone", 4],
["🎺", "trumpet", 4],
+ ["🪈", "flute", 4],
["🎸", "guitar", 4],
["🎻", "violin", 4],
["🪕", "banjo", 4],
@@ -1108,6 +1122,7 @@
["🩹", "adhesive_bandage", 6],
["🩺", "stethoscope", 6],
["🪒", "razor", 6],
+ ["🪮", "hair_pick", 6],
["🩻", "xray", 6],
["🩼", "crutch", 6],
["🧬", "dna", 6],
@@ -1156,6 +1171,7 @@
["🎊", "confetti_ball", 6],
["🎉", "tada", 6],
["🎎", "dolls", 6],
+ ["🪭", "folding_hand_fan", 6],
["🎐", "wind_chime", 6],
["🎌", "crossed_flags", 6],
["🏮", "izakaya_lantern", 6],
@@ -1237,14 +1253,17 @@
["🪧", "placard", 6],
["💯", "100", 7],
["🔢", "1234", 7],
+ ["🩷", "pink_heart", 7],
["❤️", "heart", 7],
["🧡", "orange_heart", 7],
["💛", "yellow_heart", 7],
["💚", "green_heart", 7],
+ ["🩵", "light_blue_heart", 7],
["💙", "blue_heart", 7],
["💜", "purple_heart", 7],
["🤎", "brown_heart", 7],
["🖤", "black_heart", 7],
+ ["🩶", "grey_heart", 7],
["🤍", "white_heart", 7],
["💔", "broken_heart", 7],
["❣", "heavy_heart_exclamation", 7],
@@ -1263,6 +1282,7 @@
["☪", "star_and_crescent", 7],
["🕉", "om", 7],
["☸", "wheel_of_dharma", 7],
+ ["🪯", "khanda", 7],
["✡", "star_of_david", 7],
["🔯", "six_pointed_star", 7],
["🕎", "menorah", 7],
@@ -1358,6 +1378,7 @@
["🛃", "customs", 7],
["🛄", "baggage_claim", 7],
["🛅", "left_luggage", 7],
+ ["🛜", "wireless", 7],
["♿", "wheelchair", 7],
["🚭", "no_smoking", 7],
["🚾", "wc", 7],
diff --git a/packages/frontend/src/filters/user.ts b/packages/frontend/src/filters/user.ts
index a7a13bec6e..8d20603725 100644
--- a/packages/frontend/src/filters/user.ts
+++ b/packages/frontend/src/filters/user.ts
@@ -4,7 +4,6 @@
*/
import * as Misskey from 'misskey-js';
-import * as Misskey from 'misskey-js';
import { url } from '@/config.js';
export const acct = (user: misskey.Acct) => {
diff --git a/packages/frontend/src/index.html b/packages/frontend/src/index.html
new file mode 100644
index 0000000000..8de01e4802
--- /dev/null
+++ b/packages/frontend/src/index.html
@@ -0,0 +1,35 @@
+<!--
+ SPDX-FileCopyrightText: syuilo and other misskey contributors
+ SPDX-License-Identifier: AGPL-3.0-only
+-->
+
+<!--
+ 開発モードのviteはこのファイルを起点にサーバーを起動します。
+ このファイルに書かれた [t]js のリンクと (s)cssのリンクと、その依存関係にあるファイルはビルドされます
+-->
+
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="UTF-8" />
+ <title>[DEV] Loading...</title>
+ <!-- https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP -->
+ <meta
+ http-equiv="Content-Security-Policy"
+ content="default-src 'self';
+ worker-src 'self';
+ script-src 'self' 'unsafe-eval';
+ style-src 'self' 'unsafe-inline';
+ img-src 'self' data: www.google.com xn--931a.moe localhost:3000 localhost:5173 127.0.0.1:5173 127.0.0.1:3000;
+ media-src 'self' localhost:3000 localhost:5173 127.0.0.1:5173 127.0.0.1:3000;
+ connect-src 'self' localhost:3000 localhost:5173 127.0.0.1:5173 127.0.0.1:3000;"
+ />
+ <meta property="og:site_name" content="[DEV BUILD] Misskey" />
+ <meta name="viewport" content="width=device-width, initial-scale=1">
+</head>
+
+<body>
+<div id="misskey_app"></div>
+<script type="module" src="./_dev_boot_.ts"></script>
+</body>
+</html>
diff --git a/packages/frontend/src/instance.ts b/packages/frontend/src/instance.ts
index cbfd95951c..b09264dabb 100644
--- a/packages/frontend/src/instance.ts
+++ b/packages/frontend/src/instance.ts
@@ -15,7 +15,7 @@ const cached = miLocalStorage.getItem('instance');
// TODO: instanceをリアクティブにするかは再考の余地あり
-export const instance: Misskey.entities.InstanceMetadata = reactive(cached ? JSON.parse(cached) : {
+export const instance: Misskey.entities.MetaResponse = reactive(cached ? JSON.parse(cached) : {
// TODO: set default values
});
diff --git a/packages/frontend/src/local-storage.ts b/packages/frontend/src/local-storage.ts
index 3023471b14..d95ff2119c 100644
--- a/packages/frontend/src/local-storage.ts
+++ b/packages/frontend/src/local-storage.ts
@@ -36,7 +36,8 @@ type Keys =
`themes:${string}` |
`aiscript:${string}` |
'lastEmojisFetchedAt' | // DEPRECATED, stored in indexeddb (13.9.0~)
- 'emojis' // DEPRECATED, stored in indexeddb (13.9.0~);
+ 'emojis' | // DEPRECATED, stored in indexeddb (13.9.0~);
+ `channelLastReadedAt:${string}`
export const miLocalStorage = {
getItem: (key: Keys): string | null => window.localStorage.getItem(key),
diff --git a/packages/frontend/src/navbar.ts b/packages/frontend/src/navbar.ts
index 11a0feea26..d615c751ee 100644
--- a/packages/frontend/src/navbar.ts
+++ b/packages/frontend/src/navbar.ts
@@ -4,6 +4,8 @@
*/
import { computed, reactive } from 'vue';
+import { clearCache } from './scripts/clear-cache.js';
+import { instance } from './instance.js';
import { $i } from '@/account.js';
import { miLocalStorage } from '@/local-storage.js';
import { openInstanceMenu } from '@/ui/_common_/common.js';
@@ -12,7 +14,6 @@ import * as os from '@/os.js';
import { i18n } from '@/i18n.js';
import { ui } from '@/config.js';
import { unisonReload } from '@/scripts/unison-reload.js';
-import { instance } from './instance.js';
export const navbarItemDef = reactive({
notifications: {
@@ -165,4 +166,11 @@ export const navbarItemDef = reactive({
show: computed(() => $i != null),
to: `/@${$i?.username}`,
},
+ cacheClear: {
+ title: i18n.ts.clearCache,
+ icon: 'ph-trash ph-bold ph-lg',
+ action: (ev) => {
+ clearCache();
+ },
+ },
});
diff --git a/packages/frontend/src/os.ts b/packages/frontend/src/os.ts
index 8093335a28..b02f6aa640 100644
--- a/packages/frontend/src/os.ts
+++ b/packages/frontend/src/os.ts
@@ -5,8 +5,8 @@
// TODO: なんでもかんでもos.tsに突っ込むのやめたいのでよしなに分割する
-import { pendingApiRequestsCount, api, apiExternal, apiGet } from '@/scripts/api.js';
-export { pendingApiRequestsCount, api, apiExternal, apiGet };
+import { pendingApiRequestsCount, api, apiGet } from '@/scripts/api.js';
+export { pendingApiRequestsCount, api, apiGet };
import { Component, markRaw, Ref, ref, defineAsyncComponent } from 'vue';
import { EventEmitter } from 'eventemitter3';
import insertTextAtCursor from 'insert-text-at-cursor';
@@ -546,7 +546,7 @@ export async function openEmojiPicker(src?: HTMLElement, opts, initialTextarea:
});
}
-export function popupMenu(items: MenuItem[] | Ref<MenuItem[]>, src?: HTMLElement, options?: {
+export function popupMenu(items: MenuItem[] | Ref<MenuItem[]>, src?: HTMLElement | EventTarget | null, options?: {
align?: string;
width?: number;
viaKeyboard?: boolean;
diff --git a/packages/frontend/src/pages/_error_.vue b/packages/frontend/src/pages/_error_.vue
index fe27206937..2cdf8f2e8c 100644
--- a/packages/frontend/src/pages/_error_.vue
+++ b/packages/frontend/src/pages/_error_.vue
@@ -25,7 +25,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { } from 'vue';
+import { ref, computed } from 'vue';
import * as Misskey from 'misskey-js';
import MkButton from '@/components/MkButton.vue';
import { version } from '@/config.js';
@@ -42,29 +42,29 @@ const props = withDefaults(defineProps<{
}>(), {
});
-let loaded = $ref(false);
-let serverIsDead = $ref(false);
-let meta = $ref<Misskey.entities.LiteInstanceMetadata | null>(null);
+const loaded = ref(false);
+const serverIsDead = ref(false);
+const meta = ref<Misskey.entities.MetaResponse | null>(null);
os.api('meta', {
detail: false,
}).then(res => {
- loaded = true;
- serverIsDead = false;
- meta = res;
+ loaded.value = true;
+ serverIsDead.value = false;
+ meta.value = res;
miLocalStorage.setItem('v', res.version);
}, () => {
- loaded = true;
- serverIsDead = true;
+ loaded.value = true;
+ serverIsDead.value = true;
});
function reload() {
unisonReload();
}
-const headerActions = $computed(() => []);
+const headerActions = computed(() => []);
-const headerTabs = $computed(() => []);
+const headerTabs = computed(() => []);
definePageMetadata({
title: i18n.ts.error,
diff --git a/packages/frontend/src/pages/about-sharkey.vue b/packages/frontend/src/pages/about-sharkey.vue
index e66aab2098..4160dcb09c 100644
--- a/packages/frontend/src/pages/about-sharkey.vue
+++ b/packages/frontend/src/pages/about-sharkey.vue
@@ -74,9 +74,9 @@ SPDX-License-Identifier: AGPL-3.0-only
<img src="https://avatars.githubusercontent.com/u/67428053?v=4" :class="$style.contributorAvatar">
<span :class="$style.contributorUsername">@kakkokari-gtyih</span>
</a>
- <a href="https://github.com/taichanNE30" target="_blank" :class="$style.contributor">
+ <a href="https://github.com/tai-cha" target="_blank" :class="$style.contributor">
<img src="https://avatars.githubusercontent.com/u/40626578?v=4" :class="$style.contributorAvatar">
- <span :class="$style.contributorUsername">@taichanNE30</span>
+ <span :class="$style.contributorUsername">@tai-cha</span>
</a>
</div>
</FormSection>
@@ -102,7 +102,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { nextTick, onBeforeUnmount } from 'vue';
+import { nextTick, onBeforeUnmount, ref, shallowRef, computed } from 'vue';
import { version } from '@/config.js';
import FormLink from '@/components/form/link.vue';
import FormSection from '@/components/form/section.vue';
@@ -116,21 +116,21 @@ import { definePageMetadata } from '@/scripts/page-metadata.js';
import { claimAchievement, claimedAchievements } from '@/scripts/achievements.js';
import { $i } from '@/account.js';
-let thereIsTreasure = $ref($i && !claimedAchievements.includes('foundTreasure'));
+const thereIsTreasure = ref($i && !claimedAchievements.includes('foundTreasure'));
let easterEggReady = false;
-let easterEggEmojis = $ref([]);
-let easterEggEngine = $ref(null);
-let sponsors = $ref([]);
-const containerEl = $shallowRef<HTMLElement>();
+const easterEggEmojis = ref([]);
+const easterEggEngine = ref(null);
+const sponsors = ref([]);
+const containerEl = shallowRef<HTMLElement>();
-await os.api('sponsors', { forceUpdate: true }).then((res) => sponsors.push(res.sponsor_data));
+await os.api('sponsors', { forceUpdate: true }).then((res) => sponsors.value.push(res.sponsor_data));
function iconLoaded() {
const emojis = defaultStore.state.reactions;
- const containerWidth = containerEl.offsetWidth;
+ const containerWidth = containerEl.value.offsetWidth;
for (let i = 0; i < 32; i++) {
- easterEggEmojis.push({
+ easterEggEmojis.value.push({
id: i.toString(),
top: -(128 + (Math.random() * 256)),
left: (Math.random() * containerWidth),
@@ -146,30 +146,30 @@ function iconLoaded() {
function gravity() {
if (!easterEggReady) return;
easterEggReady = false;
- easterEggEngine = physics(containerEl);
+ easterEggEngine.value = physics(containerEl.value);
}
function iLoveMisskey() {
os.post({
- initialText: 'I $[jelly ❤] #Sharkey',
+ initialText: 'I $[jelly ❤] #Misskey',
instant: true,
});
}
function getTreasure() {
- thereIsTreasure = false;
+ thereIsTreasure.value = false;
claimAchievement('foundTreasure');
}
onBeforeUnmount(() => {
- if (easterEggEngine) {
- easterEggEngine.stop();
+ if (easterEggEngine.value) {
+ easterEggEngine.value.stop();
}
});
-const headerActions = $computed(() => []);
+const headerActions = computed(() => []);
-const headerTabs = $computed(() => []);
+const headerTabs = computed(() => []);
definePageMetadata({
title: i18n.ts.aboutMisskey,
diff --git a/packages/frontend/src/pages/about.emojis.vue b/packages/frontend/src/pages/about.emojis.vue
index 867b305de0..eda6455fd6 100644
--- a/packages/frontend/src/pages/about.emojis.vue
+++ b/packages/frontend/src/pages/about.emojis.vue
@@ -36,7 +36,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { watch } from 'vue';
+import { watch, ref } from 'vue';
import * as Misskey from 'misskey-js';
import XEmoji from './emojis.emoji.vue';
import MkButton from '@/components/MkButton.vue';
@@ -47,44 +47,44 @@ import { i18n } from '@/i18n.js';
import { $i } from '@/account.js';
const customEmojiTags = getCustomEmojiTags();
-let q = $ref('');
-let searchEmojis = $ref<Misskey.entities.CustomEmoji[]>(null);
-let selectedTags = $ref(new Set());
+const q = ref('');
+const searchEmojis = ref<Misskey.entities.EmojiSimple[]>(null);
+const selectedTags = ref(new Set());
function search() {
- if ((q === '' || q == null) && selectedTags.size === 0) {
- searchEmojis = null;
+ if ((q.value === '' || q.value == null) && selectedTags.value.size === 0) {
+ searchEmojis.value = null;
return;
}
- if (selectedTags.size === 0) {
- const queryarry = q.match(/\:([a-z0-9_]*)\:/g);
+ if (selectedTags.value.size === 0) {
+ const queryarry = q.value.match(/\:([a-z0-9_]*)\:/g);
if (queryarry) {
- searchEmojis = customEmojis.value.filter(emoji =>
+ searchEmojis.value = customEmojis.value.filter(emoji =>
queryarry.includes(`:${emoji.name}:`),
);
} else {
- searchEmojis = customEmojis.value.filter(emoji => emoji.name.includes(q) || emoji.aliases.includes(q));
+ searchEmojis.value = customEmojis.value.filter(emoji => emoji.name.includes(q.value) || emoji.aliases.includes(q.value));
}
} else {
- searchEmojis = customEmojis.value.filter(emoji => (emoji.name.includes(q) || emoji.aliases.includes(q)) && [...selectedTags].every(t => emoji.aliases.includes(t)));
+ searchEmojis.value = customEmojis.value.filter(emoji => (emoji.name.includes(q.value) || emoji.aliases.includes(q.value)) && [...selectedTags.value].every(t => emoji.aliases.includes(t)));
}
}
function toggleTag(tag) {
- if (selectedTags.has(tag)) {
- selectedTags.delete(tag);
+ if (selectedTags.value.has(tag)) {
+ selectedTags.value.delete(tag);
} else {
- selectedTags.add(tag);
+ selectedTags.value.add(tag);
}
}
-watch($$(q), () => {
+watch(q, () => {
search();
});
-watch($$(selectedTags), () => {
+watch(selectedTags, () => {
search();
}, { deep: true });
</script>
diff --git a/packages/frontend/src/pages/about.federation.vue b/packages/frontend/src/pages/about.federation.vue
index 27f7784007..0de000ee3e 100644
--- a/packages/frontend/src/pages/about.federation.vue
+++ b/packages/frontend/src/pages/about.federation.vue
@@ -52,7 +52,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { computed } from 'vue';
+import { computed, ref } from 'vue';
import MkInput from '@/components/MkInput.vue';
import MkSelect from '@/components/MkSelect.vue';
import MkPagination, { Paging } from '@/components/MkPagination.vue';
@@ -60,26 +60,26 @@ import MkInstanceCardMini from '@/components/MkInstanceCardMini.vue';
import FormSplit from '@/components/form/split.vue';
import { i18n } from '@/i18n.js';
-let host = $ref('');
-let state = $ref('federating');
-let sort = $ref('+pubSub');
+const host = ref('');
+const state = ref('federating');
+const sort = ref('+pubSub');
const pagination = {
endpoint: 'federation/instances' as const,
limit: 10,
displayLimit: 50,
offsetMode: true,
params: computed(() => ({
- sort: sort,
- host: host !== '' ? host : null,
+ sort: sort.value,
+ host: host.value !== '' ? host.value : null,
...(
- state === 'federating' ? { federating: true } :
- state === 'subscribing' ? { subscribing: true } :
- state === 'publishing' ? { publishing: true } :
- state === 'suspended' ? { suspended: true } :
- state === 'blocked' ? { blocked: true } :
- state === 'silenced' ? { silenced: true } :
- state === 'notResponding' ? { notResponding: true } :
- state === 'nsfw' ? { nsfw: true } :
+ state.value === 'federating' ? { federating: true } :
+ state.value === 'subscribing' ? { subscribing: true } :
+ state.value === 'publishing' ? { publishing: true } :
+ state.value === 'suspended' ? { suspended: true } :
+ state.value === 'blocked' ? { blocked: true } :
+ state.value === 'silenced' ? { silenced: true } :
+ state.value === 'notResponding' ? { notResponding: true } :
+ state.value === 'nsfw' ? { nsfw: true } :
{}),
})),
} as Paging;
diff --git a/packages/frontend/src/pages/about.vue b/packages/frontend/src/pages/about.vue
index e3586ba587..ff6ed2a624 100644
--- a/packages/frontend/src/pages/about.vue
+++ b/packages/frontend/src/pages/about.vue
@@ -102,7 +102,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { computed, watch } from 'vue';
+import { computed, watch, ref } from 'vue';
import XEmojis from './about.emojis.vue';
import XFederation from './about.federation.vue';
import { version, host } from '@/config.js';
@@ -126,23 +126,23 @@ const props = withDefaults(defineProps<{
initialTab: 'overview',
});
-let stats = $ref(null);
-let tab = $ref(props.initialTab);
+const stats = ref(null);
+const tab = ref(props.initialTab);
-watch($$(tab), () => {
- if (tab === 'charts') {
+watch(tab, () => {
+ if (tab.value === 'charts') {
claimAchievement('viewInstanceChart');
}
});
const initStats = () => os.api('stats', {
}).then((res) => {
- stats = res;
+ stats.value = res;
});
-const headerActions = $computed(() => []);
+const headerActions = computed(() => []);
-const headerTabs = $computed(() => [{
+const headerTabs = computed(() => [{
key: 'overview',
title: i18n.ts.overview,
}, {
diff --git a/packages/frontend/src/pages/admin-file.vue b/packages/frontend/src/pages/admin-file.vue
index b894c9bbe9..8479b0ddea 100644
--- a/packages/frontend/src/pages/admin-file.vue
+++ b/packages/frontend/src/pages/admin-file.vue
@@ -67,7 +67,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { computed } from 'vue';
+import { computed, ref } from 'vue';
import MkButton from '@/components/MkButton.vue';
import MkSwitch from '@/components/MkSwitch.vue';
import MkObjectView from '@/components/MkObjectView.vue';
@@ -82,19 +82,19 @@ import { i18n } from '@/i18n.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';
import { iAmAdmin, iAmModerator } from '@/account.js';
-let tab = $ref('overview');
-let file: any = $ref(null);
-let info: any = $ref(null);
-let isSensitive: boolean = $ref(false);
+const tab = ref('overview');
+const file = ref<any>(null);
+const info = ref<any>(null);
+const isSensitive = ref<boolean>(false);
const props = defineProps<{
fileId: string,
}>();
async function fetch() {
- file = await os.api('drive/files/show', { fileId: props.fileId });
- info = await os.api('admin/drive/show-file', { fileId: props.fileId });
- isSensitive = file.isSensitive;
+ file.value = await os.api('drive/files/show', { fileId: props.fileId });
+ info.value = await os.api('admin/drive/show-file', { fileId: props.fileId });
+ isSensitive.value = file.value.isSensitive;
}
fetch();
@@ -102,29 +102,29 @@ fetch();
async function del() {
const { canceled } = await os.confirm({
type: 'warning',
- text: i18n.t('removeAreYouSure', { x: file.name }),
+ text: i18n.t('removeAreYouSure', { x: file.value.name }),
});
if (canceled) return;
os.apiWithDialog('drive/files/delete', {
- fileId: file.id,
+ fileId: file.value.id,
});
}
async function toggleIsSensitive(v) {
await os.api('drive/files/update', { fileId: props.fileId, isSensitive: v });
- isSensitive = v;
+ isSensitive.value = v;
}
-const headerActions = $computed(() => [{
+const headerActions = computed(() => [{
text: i18n.ts.openInNewTab,
icon: 'ph-arrow-square-out ph-bold ph-lg',
handler: () => {
- window.open(file.url, '_blank');
+ window.open(file.value.url, '_blank', 'noopener');
},
}]);
-const headerTabs = $computed(() => [{
+const headerTabs = computed(() => [{
key: 'overview',
title: i18n.ts.overview,
icon: 'ph-info ph-bold ph-lg',
@@ -139,7 +139,7 @@ const headerTabs = $computed(() => [{
}]);
definePageMetadata(computed(() => ({
- title: file ? i18n.ts.file + ': ' + file.name : i18n.ts.file,
+ title: file.value ? i18n.ts.file + ': ' + file.value.name : i18n.ts.file,
icon: 'ph-file ph-bold ph-lg',
})));
</script>
diff --git a/packages/frontend/src/pages/admin-user.vue b/packages/frontend/src/pages/admin-user.vue
index 25547117a4..5225b4a831 100644
--- a/packages/frontend/src/pages/admin-user.vue
+++ b/packages/frontend/src/pages/admin-user.vue
@@ -109,6 +109,10 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
</MkFolder>
+ <div>
+ <MkButton v-if="iAmModerator" inline danger style="margin-right: 8px;" @click="unsetUserAvatar"><i class="ti ti-user-circle"></i> {{ i18n.ts.unsetUserAvatar }}</MkButton>
+ <MkButton v-if="iAmModerator" inline danger @click="unsetUserBanner"><i class="ti ti-photo"></i> {{ i18n.ts.unsetUserBanner }}</MkButton>
+ </div>
<MkButton v-if="$i.isAdmin" inline danger @click="deleteAccount">{{ i18n.ts.deleteAccount }}</MkButton>
</div>
</FormSection>
@@ -117,7 +121,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<div v-else-if="tab === 'roles'" class="_gaps">
<MkButton v-if="user.host == null" primary rounded @click="assignRole"><i class="ph-plus ph-bold ph-lg"></i> {{ i18n.ts.assign }}</MkButton>
- <div v-for="role in info.roles" :key="role.id" :class="$style.roleItem">
+ <div v-for="role in info.roles" :key="role.id">
<div :class="$style.roleItemMain">
<MkRolePreview :class="$style.role" :role="role" :forModeration="true"/>
<button class="_button" :class="$style.roleToggle" @click="toggleRoleItem(role)"><i class="ph-caret-down ph-bold ph-lg"></i></button>
@@ -186,7 +190,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { computed, defineAsyncComponent, watch } from 'vue';
+import { computed, defineAsyncComponent, watch, ref } from 'vue';
import * as Misskey from 'misskey-js';
import MkChart from '@/components/MkChart.vue';
import MkObjectView from '@/components/MkObjectView.vue';
@@ -203,12 +207,12 @@ import MkFileListForAdmin from '@/components/MkFileListForAdmin.vue';
import MkInfo from '@/components/MkInfo.vue';
import * as os from '@/os.js';
import { url } from '@/config.js';
-import { userPage, acct } from '@/filters/user.js';
+import { acct } from '@/filters/user.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';
import { i18n } from '@/i18n.js';
import { iAmAdmin, $i } from '@/account.js';
import MkRolePreview from '@/components/MkRolePreview.vue';
-import MkPagination, { Paging } from '@/components/MkPagination.vue';
+import MkPagination from '@/components/MkPagination.vue';
const props = withDefaults(defineProps<{
userId: string;
@@ -217,20 +221,19 @@ const props = withDefaults(defineProps<{
initialTab: 'overview',
});
-let tab = $ref(props.initialTab);
-let chartSrc = $ref('per-user-notes');
-let user = $ref<null | Misskey.entities.UserDetailed>();
-let init = $ref<ReturnType<typeof createFetcher>>();
-let info = $ref();
-let ips = $ref(null);
-let ap = $ref(null);
-let moderator = $ref(false);
-let silenced = $ref(false);
-let approved = $ref(false);
-let suspended = $ref(false);
-let markedAsNSFW = $ref(false);
-let moderationNote = $ref('');
-
+const tab = ref(props.initialTab);
+const chartSrc = ref('per-user-notes');
+const user = ref<null | Misskey.entities.UserDetailed>();
+const init = ref<ReturnType<typeof createFetcher>>();
+const info = ref();
+const ips = ref(null);
+const ap = ref(null);
+const moderator = ref(false);
+const silenced = ref(false);
+const approved = ref(false);
+const suspended = ref(false);
+const markedAsNSFW = ref(false);
+const moderationNote = ref('');
const filesPagination = {
endpoint: 'admin/drive/files' as const,
limit: 10,
@@ -245,7 +248,7 @@ const announcementsPagination = {
userId: props.userId,
})),
};
-let expandedRoles = $ref([]);
+const expandedRoles = ref([]);
function createFetcher() {
return () => Promise.all([os.api('users/show', {
@@ -255,29 +258,28 @@ function createFetcher() {
}), iAmAdmin ? os.api('admin/get-user-ips', {
userId: props.userId,
}) : Promise.resolve(null)]).then(([_user, _info, _ips]) => {
- user = _user;
- info = _info;
- ips = _ips;
- moderator = info.isModerator;
- silenced = info.isSilenced;
- approved = info.approved;
- suspended = info.isSuspended;
- moderationNote = info.moderationNote;
- markedAsNSFW = info.alwaysMarkNsfw;
+ user.value = _user;
+ info.value = _info;
+ ips.value = _ips;
+ moderator.value = info.value.isModerator;
+ silenced.value = info.value.isSilenced;
+ approved.value = info.value.approved;
+ suspended.value = info.value.isSuspended;
+ moderationNote.value = info.value.moderationNote;
- watch($$(moderationNote), async () => {
- await os.api('admin/update-user-note', { userId: user.id, text: moderationNote });
+ watch(moderationNote, async () => {
+ await os.api('admin/update-user-note', { userId: user.value.id, text: moderationNote.value });
await refreshUser();
});
});
}
function refreshUser() {
- init = createFetcher();
+ init.value = createFetcher();
}
async function updateRemoteUser() {
- await os.apiWithDialog('federation/update-remote-user', { userId: user.id });
+ await os.apiWithDialog('federation/update-remote-user', { userId: user.value.id });
refreshUser();
}
@@ -290,7 +292,7 @@ async function resetPassword() {
return;
} else {
const { password } = await os.api('admin/reset-password', {
- userId: user.id,
+ userId: user.value.id,
});
os.alert({
type: 'success',
@@ -305,9 +307,9 @@ async function toggleNSFW(v) {
text: v ? i18n.ts.nsfwConfirm : i18n.ts.unNsfwConfirm,
});
if (confirm.canceled) {
- markedAsNSFW = !v;
+ markedAsNSFW.value = !v;
} else {
- await os.api(v ? 'admin/nsfw-user' : 'admin/unnsfw-user', { userId: user.id });
+ await os.api(v ? 'admin/nsfw-user' : 'admin/unnsfw-user', { userId: user.value.id });
await refreshUser();
}
}
@@ -318,9 +320,9 @@ async function toggleSilence(v) {
text: v ? i18n.ts.silenceConfirm : i18n.ts.unsilenceConfirm,
});
if (confirm.canceled) {
- silenced = !v;
+ silenced.value = !v;
} else {
- await os.api(v ? 'admin/silence-user' : 'admin/unsilence-user', { userId: user.id });
+ await os.api(v ? 'admin/silence-user' : 'admin/unsilence-user', { userId: user.value.id });
await refreshUser();
}
}
@@ -331,13 +333,51 @@ async function toggleSuspend(v) {
text: v ? i18n.ts.suspendConfirm : i18n.ts.unsuspendConfirm,
});
if (confirm.canceled) {
- suspended = !v;
+ suspended.value = !v;
} else {
- await os.api(v ? 'admin/suspend-user' : 'admin/unsuspend-user', { userId: user.id });
+ await os.api(v ? 'admin/suspend-user' : 'admin/unsuspend-user', { userId: user.value.id });
await refreshUser();
}
}
+async function unsetUserAvatar() {
+ const confirm = await os.confirm({
+ type: 'warning',
+ text: i18n.ts.unsetUserAvatarConfirm,
+ });
+ if (confirm.canceled) return;
+ const process = async () => {
+ await os.api('admin/unset-user-avatar', { userId: user.value.id });
+ os.success();
+ };
+ await process().catch(err => {
+ os.alert({
+ type: 'error',
+ text: err.toString(),
+ });
+ });
+ refreshUser();
+}
+
+async function unsetUserBanner() {
+ const confirm = await os.confirm({
+ type: 'warning',
+ text: i18n.ts.unsetUserBannerConfirm,
+ });
+ if (confirm.canceled) return;
+ const process = async () => {
+ await os.api('admin/unset-user-banner', { userId: user.value.id });
+ os.success();
+ };
+ await process().catch(err => {
+ os.alert({
+ type: 'error',
+ text: err.toString(),
+ });
+ });
+ refreshUser();
+}
+
async function deleteAllFiles() {
const confirm = await os.confirm({
type: 'warning',
@@ -345,7 +385,7 @@ async function deleteAllFiles() {
});
if (confirm.canceled) return;
const process = async () => {
- await os.api('admin/delete-all-files-of-a-user', { userId: user.id });
+ await os.api('admin/delete-all-files-of-a-user', { userId: user.value.id });
os.success();
};
await process().catch(err => {
@@ -365,13 +405,13 @@ async function deleteAccount() {
if (confirm.canceled) return;
const typed = await os.inputText({
- text: i18n.t('typeToConfirm', { x: user?.username }),
+ text: i18n.t('typeToConfirm', { x: user.value?.username }),
});
if (typed.canceled) return;
- if (typed.result === user?.username) {
+ if (typed.result === user.value?.username) {
await os.apiWithDialog('admin/delete-account', {
- userId: user.id,
+ userId: user.value.id,
});
} else {
os.alert({
@@ -414,7 +454,7 @@ async function assignRole() {
: period === 'oneMonth' ? Date.now() + (1000 * 60 * 60 * 24 * 30)
: null;
- await os.apiWithDialog('admin/roles/assign', { roleId, userId: user.id, expiresAt });
+ await os.apiWithDialog('admin/roles/assign', { roleId, userId: user.value.id, expiresAt });
refreshUser();
}
@@ -424,50 +464,50 @@ async function unassignRole(role, ev) {
icon: 'ph-x ph-bold ph-lg',
danger: true,
action: async () => {
- await os.apiWithDialog('admin/roles/unassign', { roleId: role.id, userId: user.id });
+ await os.apiWithDialog('admin/roles/unassign', { roleId: role.id, userId: user.value.id });
refreshUser();
},
}], ev.currentTarget ?? ev.target);
}
function toggleRoleItem(role) {
- if (expandedRoles.includes(role.id)) {
- expandedRoles = expandedRoles.filter(x => x !== role.id);
+ if (expandedRoles.value.includes(role.id)) {
+ expandedRoles.value = expandedRoles.value.filter(x => x !== role.id);
} else {
- expandedRoles.push(role.id);
+ expandedRoles.value.push(role.id);
}
}
function createAnnouncement() {
os.popup(defineAsyncComponent(() => import('@/components/MkUserAnnouncementEditDialog.vue')), {
- user,
+ user: user.value,
}, {}, 'closed');
}
function editAnnouncement(announcement) {
os.popup(defineAsyncComponent(() => import('@/components/MkUserAnnouncementEditDialog.vue')), {
- user,
+ user: user.value,
announcement,
}, {}, 'closed');
}
watch(() => props.userId, () => {
- init = createFetcher();
+ init.value = createFetcher();
}, {
immediate: true,
});
-watch($$(user), () => {
+watch(user, () => {
os.api('ap/get', {
- uri: user.uri ?? `${url}/users/${user.id}`,
+ uri: user.value.uri ?? `${url}/users/${user.value.id}`,
}).then(res => {
- ap = res;
+ ap.value = res;
});
});
-const headerActions = $computed(() => []);
+const headerActions = computed(() => []);
-const headerTabs = $computed(() => [{
+const headerTabs = computed(() => [{
key: 'overview',
title: i18n.ts.overview,
icon: 'ph-info ph-bold ph-lg',
@@ -494,7 +534,7 @@ const headerTabs = $computed(() => [{
}]);
definePageMetadata(computed(() => ({
- title: user ? acct(user) : i18n.ts.userInfo,
+ title: user.value ? acct(user.value) : i18n.ts.userInfo,
icon: 'ph-warning-circle ph-bold ph-lg',
})));
</script>
@@ -609,9 +649,6 @@ definePageMetadata(computed(() => ({
}
}
-.roleItem {
-}
-
.roleItemMain {
display: flex;
}
diff --git a/packages/frontend/src/pages/admin/_header_.vue b/packages/frontend/src/pages/admin/_header_.vue
index 43bfbeb870..353030b1b9 100644
--- a/packages/frontend/src/pages/admin/_header_.vue
+++ b/packages/frontend/src/pages/admin/_header_.vue
@@ -69,7 +69,7 @@ const metadata = injectPageMetadata();
const el = shallowRef<HTMLElement>(null);
const tabRefs = {};
-const tabHighlightEl = $shallowRef<HTMLElement | null>(null);
+const tabHighlightEl = shallowRef<HTMLElement | null>(null);
const bg = ref(null);
const height = ref(0);
const hasTabs = computed(() => {
@@ -131,13 +131,13 @@ onMounted(() => {
watch(() => [props.tab, props.tabs], () => {
nextTick(() => {
const tabEl = tabRefs[props.tab];
- if (tabEl && tabHighlightEl) {
+ if (tabEl && tabHighlightEl.value) {
// offsetWidth や offsetLeft は少数を丸めてしまうため getBoundingClientRect を使う必要がある
// https://developer.mozilla.org/ja/docs/Web/API/HTMLElement/offsetWidth#%E5%80%A4
const parentRect = tabEl.parentElement.getBoundingClientRect();
const rect = tabEl.getBoundingClientRect();
- tabHighlightEl.style.width = rect.width + 'px';
- tabHighlightEl.style.left = (rect.left - parentRect.left) + 'px';
+ tabHighlightEl.value.style.width = rect.width + 'px';
+ tabHighlightEl.value.style.left = (rect.left - parentRect.left) + 'px';
}
});
}, {
diff --git a/packages/frontend/src/pages/admin/abuses.vue b/packages/frontend/src/pages/admin/abuses.vue
index d670cc7913..92688989d2 100644
--- a/packages/frontend/src/pages/admin/abuses.vue
+++ b/packages/frontend/src/pages/admin/abuses.vue
@@ -52,7 +52,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { computed } from 'vue';
+import { computed, shallowRef, ref } from 'vue';
import XHeader from './_header_.vue';
import MkSelect from '@/components/MkSelect.vue';
@@ -61,31 +61,31 @@ import XAbuseReport from '@/components/MkAbuseReport.vue';
import { i18n } from '@/i18n.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';
-let reports = $shallowRef<InstanceType<typeof MkPagination>>();
+const reports = shallowRef<InstanceType<typeof MkPagination>>();
-let state = $ref('unresolved');
-let reporterOrigin = $ref('combined');
-let targetUserOrigin = $ref('combined');
-let searchUsername = $ref('');
-let searchHost = $ref('');
+const state = ref('unresolved');
+const reporterOrigin = ref('combined');
+const targetUserOrigin = ref('combined');
+const searchUsername = ref('');
+const searchHost = ref('');
const pagination = {
endpoint: 'admin/abuse-user-reports' as const,
limit: 10,
params: computed(() => ({
- state,
- reporterOrigin,
- targetUserOrigin,
+ state: state.value,
+ reporterOrigin: reporterOrigin.value,
+ targetUserOrigin: targetUserOrigin.value,
})),
};
function resolved(reportId) {
- reports.removeItem(reportId);
+ reports.value.removeItem(reportId);
}
-const headerActions = $computed(() => []);
+const headerActions = computed(() => []);
-const headerTabs = $computed(() => []);
+const headerTabs = computed(() => []);
definePageMetadata({
title: i18n.ts.abuseReports,
diff --git a/packages/frontend/src/pages/admin/ads.vue b/packages/frontend/src/pages/admin/ads.vue
index c45d0f8b04..9de9da7d98 100644
--- a/packages/frontend/src/pages/admin/ads.vue
+++ b/packages/frontend/src/pages/admin/ads.vue
@@ -9,12 +9,15 @@ SPDX-License-Identifier: AGPL-3.0-only
<XHeader :actions="headerActions" :tabs="headerTabs"/>
</template>
<MkSpacer :contentMax="900">
- <MkSwitch :modelValue="publishing" @update:modelValue="onChangePublishing">
- {{ i18n.ts.publishing }}
- </MkSwitch>
+ <MkSelect v-model="filterType" :class="$style.input" @update:modelValue="filterItems">
+ <template #label>{{ i18n.ts.state }}</template>
+ <option value="all">{{ i18n.ts.all }}</option>
+ <option value="publishing">{{ i18n.ts.publishing }}</option>
+ <option value="expired">{{ i18n.ts.expired }}</option>
+ </MkSelect>
<div>
<div v-for="ad in ads" class="_panel _gaps_m" :class="$style.ad">
- <MkAd v-if="ad.url" :specify="ad"/>
+ <MkAd v-if="ad.url" :key="ad.id" :specify="ad"/>
<MkInput v-model="ad.url" type="url">
<template #label>URL</template>
</MkInput>
@@ -82,43 +85,53 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { } from 'vue';
+import { ref, computed } from 'vue';
import XHeader from './_header_.vue';
import MkButton from '@/components/MkButton.vue';
import MkInput from '@/components/MkInput.vue';
import MkTextarea from '@/components/MkTextarea.vue';
import MkRadios from '@/components/MkRadios.vue';
import MkFolder from '@/components/MkFolder.vue';
-import MkSwitch from '@/components/MkSwitch.vue';
+import MkSelect from '@/components/MkSelect.vue';
import FormSplit from '@/components/form/split.vue';
import * as os from '@/os.js';
import { i18n } from '@/i18n.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';
-let ads: any[] = $ref([]);
+const ads = ref<any[]>([]);
// ISO形式はTZがUTCになってしまうので、TZ分ずらして時間を初期化
const localTime = new Date();
const localTimeDiff = localTime.getTimezoneOffset() * 60 * 1000;
const daysOfWeek: string[] = [i18n.ts._weekday.sunday, i18n.ts._weekday.monday, i18n.ts._weekday.tuesday, i18n.ts._weekday.wednesday, i18n.ts._weekday.thursday, i18n.ts._weekday.friday, i18n.ts._weekday.saturday];
-let publishing = false;
+const filterType = ref('all');
+let publishing: boolean | null = null;
os.api('admin/ad/list', { publishing: publishing }).then(adsResponse => {
- ads = adsResponse.map(r => {
- const exdate = new Date(r.expiresAt);
- const stdate = new Date(r.startsAt);
- exdate.setMilliseconds(exdate.getMilliseconds() - localTimeDiff);
- stdate.setMilliseconds(stdate.getMilliseconds() - localTimeDiff);
- return {
- ...r,
- expiresAt: exdate.toISOString().slice(0, 16),
- startsAt: stdate.toISOString().slice(0, 16),
- };
- });
+ if (adsResponse != null) {
+ ads.value = adsResponse.map(r => {
+ const exdate = new Date(r.expiresAt);
+ const stdate = new Date(r.startsAt);
+ exdate.setMilliseconds(exdate.getMilliseconds() - localTimeDiff);
+ stdate.setMilliseconds(stdate.getMilliseconds() - localTimeDiff);
+ return {
+ ...r,
+ expiresAt: exdate.toISOString().slice(0, 16),
+ startsAt: stdate.toISOString().slice(0, 16),
+ };
+ });
+ }
});
-const onChangePublishing = (v) => {
- publishing = v;
+const filterItems = (v) => {
+ if (v === 'publishing') {
+ publishing = true;
+ } else if (v === 'expired') {
+ publishing = false;
+ } else {
+ publishing = null;
+ }
+
refresh();
};
@@ -128,7 +141,7 @@ function toggleDayOfWeek(ad, index) {
}
function add() {
- ads.unshift({
+ ads.value.unshift({
id: null,
memo: '',
place: 'square',
@@ -148,7 +161,7 @@ function remove(ad) {
text: i18n.t('removeAreYouSure', { x: ad.url }),
}).then(({ canceled }) => {
if (canceled) return;
- ads = ads.filter(x => x !== ad);
+ ads.value = ads.value.filter(x => x !== ad);
if (ad.id == null) return;
os.apiWithDialog('admin/ad/delete', {
id: ad.id,
@@ -196,8 +209,9 @@ function save(ad) {
}
function more() {
- os.api('admin/ad/list', { untilId: ads.reduce((acc, ad) => ad.id != null ? ad : acc).id, publishing: publishing }).then(adsResponse => {
- ads = ads.concat(adsResponse.map(r => {
+ os.api('admin/ad/list', { untilId: ads.value.reduce((acc, ad) => ad.id != null ? ad : acc).id, publishing: publishing }).then(adsResponse => {
+ if (adsResponse == null) return;
+ ads.value = ads.value.concat(adsResponse.map(r => {
const exdate = new Date(r.expiresAt);
const stdate = new Date(r.startsAt);
exdate.setMilliseconds(exdate.getMilliseconds() - localTimeDiff);
@@ -213,7 +227,8 @@ function more() {
function refresh() {
os.api('admin/ad/list', { publishing: publishing }).then(adsResponse => {
- ads = adsResponse.map(r => {
+ if (adsResponse == null) return;
+ ads.value = adsResponse.map(r => {
const exdate = new Date(r.expiresAt);
const stdate = new Date(r.startsAt);
exdate.setMilliseconds(exdate.getMilliseconds() - localTimeDiff);
@@ -229,14 +244,14 @@ function refresh() {
refresh();
-const headerActions = $computed(() => [{
+const headerActions = computed(() => [{
asFullButton: true,
icon: 'ph-plus ph-bold ph-lg',
text: i18n.ts.add,
handler: add,
}]);
-const headerTabs = $computed(() => []);
+const headerTabs = computed(() => []);
definePageMetadata({
title: i18n.ts.ads,
@@ -252,4 +267,7 @@ definePageMetadata({
margin-bottom: var(--margin);
}
}
+.input {
+ margin-bottom: 32px;
+}
</style>
diff --git a/packages/frontend/src/pages/admin/announcements.vue b/packages/frontend/src/pages/admin/announcements.vue
index 77649c6c4a..931bd9bbc8 100644
--- a/packages/frontend/src/pages/admin/announcements.vue
+++ b/packages/frontend/src/pages/admin/announcements.vue
@@ -25,7 +25,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkInput v-model="announcement.title">
<template #label>{{ i18n.ts.title }}</template>
</MkInput>
- <MkTextarea v-model="announcement.text">
+ <MkTextarea v-model="announcement.text" mfmAutocomplete :mfmPreview="true">
<template #label>{{ i18n.ts.text }}</template>
</MkTextarea>
<MkInput v-model="announcement.imageUrl" type="url">
@@ -71,11 +71,10 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { } from 'vue';
+import { ref, computed } from 'vue';
import XHeader from './_header_.vue';
import MkButton from '@/components/MkButton.vue';
import MkInput from '@/components/MkInput.vue';
-import MkTextarea from '@/components/MkTextarea.vue';
import MkSwitch from '@/components/MkSwitch.vue';
import MkRadios from '@/components/MkRadios.vue';
import MkInfo from '@/components/MkInfo.vue';
@@ -83,15 +82,16 @@ import * as os from '@/os.js';
import { i18n } from '@/i18n.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';
import MkFolder from '@/components/MkFolder.vue';
+import MkTextarea from '@/components/MkTextarea.vue';
-let announcements: any[] = $ref([]);
+const announcements = ref<any[]>([]);
os.api('admin/announcements/list').then(announcementResponse => {
- announcements = announcementResponse;
+ announcements.value = announcementResponse;
});
function add() {
- announcements.unshift({
+ announcements.value.unshift({
_id: Math.random().toString(36),
id: null,
title: 'New announcement',
@@ -111,7 +111,7 @@ function del(announcement) {
text: i18n.t('deleteAreYouSure', { x: announcement.title }),
}).then(({ canceled }) => {
if (canceled) return;
- announcements = announcements.filter(x => x !== announcement);
+ announcements.value = announcements.value.filter(x => x !== announcement);
os.api('admin/announcements/delete', announcement);
});
}
@@ -134,27 +134,27 @@ async function save(announcement) {
}
function more() {
- os.api('admin/announcements/list', { untilId: announcements.reduce((acc, announcement) => announcement.id != null ? announcement : acc).id }).then(announcementResponse => {
- announcements = announcements.concat(announcementResponse);
+ os.api('admin/announcements/list', { untilId: announcements.value.reduce((acc, announcement) => announcement.id != null ? announcement : acc).id }).then(announcementResponse => {
+ announcements.value = announcements.value.concat(announcementResponse);
});
}
function refresh() {
os.api('admin/announcements/list').then(announcementResponse => {
- announcements = announcementResponse;
+ announcements.value = announcementResponse;
});
}
refresh();
-const headerActions = $computed(() => [{
+const headerActions = computed(() => [{
asFullButton: true,
icon: 'ph-plus ph-bold ph-lg',
text: i18n.ts.add,
handler: add,
}]);
-const headerTabs = $computed(() => []);
+const headerTabs = computed(() => []);
definePageMetadata({
title: i18n.ts.announcements,
diff --git a/packages/frontend/src/pages/admin/approvals.vue b/packages/frontend/src/pages/admin/approvals.vue
index 5d9c9de03b..7d0535bd7f 100644
--- a/packages/frontend/src/pages/admin/approvals.vue
+++ b/packages/frontend/src/pages/admin/approvals.vue
@@ -44,9 +44,9 @@ function deleted(id: string) {
}
}
-const headerActions = $computed(() => []);
+const headerActions = computed(() => []);
-const headerTabs = $computed(() => []);
+const headerTabs = computed(() => []);
definePageMetadata(computed(() => ({
title: i18n.ts.approvals,
diff --git a/packages/frontend/src/pages/admin/bot-protection.vue b/packages/frontend/src/pages/admin/bot-protection.vue
index 450c0ec663..034a6fdcc5 100644
--- a/packages/frontend/src/pages/admin/bot-protection.vue
+++ b/packages/frontend/src/pages/admin/bot-protection.vue
@@ -64,7 +64,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { defineAsyncComponent } from 'vue';
+import { defineAsyncComponent, ref } from 'vue';
import MkRadios from '@/components/MkRadios.vue';
import MkInput from '@/components/MkInput.vue';
import MkButton from '@/components/MkButton.vue';
@@ -76,37 +76,37 @@ import { i18n } from '@/i18n.js';
const MkCaptcha = defineAsyncComponent(() => import('@/components/MkCaptcha.vue'));
-let provider = $ref(null);
-let hcaptchaSiteKey: string | null = $ref(null);
-let hcaptchaSecretKey: string | null = $ref(null);
-let recaptchaSiteKey: string | null = $ref(null);
-let recaptchaSecretKey: string | null = $ref(null);
-let turnstileSiteKey: string | null = $ref(null);
-let turnstileSecretKey: string | null = $ref(null);
+const provider = ref(null);
+const hcaptchaSiteKey = ref<string | null>(null);
+const hcaptchaSecretKey = ref<string | null>(null);
+const recaptchaSiteKey = ref<string | null>(null);
+const recaptchaSecretKey = ref<string | null>(null);
+const turnstileSiteKey = ref<string | null>(null);
+const turnstileSecretKey = ref<string | null>(null);
async function init() {
const meta = await os.api('admin/meta');
- hcaptchaSiteKey = meta.hcaptchaSiteKey;
- hcaptchaSecretKey = meta.hcaptchaSecretKey;
- recaptchaSiteKey = meta.recaptchaSiteKey;
- recaptchaSecretKey = meta.recaptchaSecretKey;
- turnstileSiteKey = meta.turnstileSiteKey;
- turnstileSecretKey = meta.turnstileSecretKey;
+ hcaptchaSiteKey.value = meta.hcaptchaSiteKey;
+ hcaptchaSecretKey.value = meta.hcaptchaSecretKey;
+ recaptchaSiteKey.value = meta.recaptchaSiteKey;
+ recaptchaSecretKey.value = meta.recaptchaSecretKey;
+ turnstileSiteKey.value = meta.turnstileSiteKey;
+ turnstileSecretKey.value = meta.turnstileSecretKey;
- provider = meta.enableHcaptcha ? 'hcaptcha' : meta.enableRecaptcha ? 'recaptcha' : meta.enableTurnstile ? 'turnstile' : null;
+ provider.value = meta.enableHcaptcha ? 'hcaptcha' : meta.enableRecaptcha ? 'recaptcha' : meta.enableTurnstile ? 'turnstile' : null;
}
function save() {
os.apiWithDialog('admin/update-meta', {
- enableHcaptcha: provider === 'hcaptcha',
- hcaptchaSiteKey,
- hcaptchaSecretKey,
- enableRecaptcha: provider === 'recaptcha',
- recaptchaSiteKey,
- recaptchaSecretKey,
- enableTurnstile: provider === 'turnstile',
- turnstileSiteKey,
- turnstileSecretKey,
+ enableHcaptcha: provider.value === 'hcaptcha',
+ hcaptchaSiteKey: hcaptchaSiteKey.value,
+ hcaptchaSecretKey: hcaptchaSecretKey.value,
+ enableRecaptcha: provider.value === 'recaptcha',
+ recaptchaSiteKey: recaptchaSiteKey.value,
+ recaptchaSecretKey: recaptchaSecretKey.value,
+ enableTurnstile: provider.value === 'turnstile',
+ turnstileSiteKey: turnstileSiteKey.value,
+ turnstileSecretKey: turnstileSecretKey.value,
}).then(() => {
fetchInstance();
});
diff --git a/packages/frontend/src/pages/admin/branding.vue b/packages/frontend/src/pages/admin/branding.vue
index 96a2d8a300..3750a84aac 100644
--- a/packages/frontend/src/pages/admin/branding.vue
+++ b/packages/frontend/src/pages/admin/branding.vue
@@ -101,14 +101,11 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { } from 'vue';
+import { ref, computed } from 'vue';
import JSON5 from 'json5';
import XHeader from './_header_.vue';
-import MkSwitch from '@/components/MkSwitch.vue';
import MkInput from '@/components/MkInput.vue';
import MkTextarea from '@/components/MkTextarea.vue';
-import FormSection from '@/components/form/section.vue';
-import FormSplit from '@/components/form/split.vue';
import FromSlot from '@/components/form/slot.vue';
import FormSuspense from '@/components/form/suspense.vue';
import * as os from '@/os.js';
@@ -119,51 +116,51 @@ import MkButton from '@/components/MkButton.vue';
import MkColorInput from '@/components/MkColorInput.vue';
import { host } from '@/config.js';
-let iconUrl: string | null = $ref(null);
-let app192IconUrl: string | null = $ref(null);
-let app512IconUrl: string | null = $ref(null);
-let bannerUrl: string | null = $ref(null);
-let backgroundImageUrl: string | null = $ref(null);
-let themeColor: any = $ref(null);
-let defaultLightTheme: any = $ref(null);
-let defaultDarkTheme: any = $ref(null);
-let defaultLike: any = $ref(null);
-let serverErrorImageUrl: string | null = $ref(null);
-let infoImageUrl: string | null = $ref(null);
-let notFoundImageUrl: string | null = $ref(null);
-let manifestJsonOverride: string = $ref('{}');
+const iconUrl = ref<string | null>(null);
+const app192IconUrl = ref<string | null>(null);
+const app512IconUrl = ref<string | null>(null);
+const bannerUrl = ref<string | null>(null);
+const backgroundImageUrl = ref<string | null>(null);
+const themeColor = ref<any>(null);
+const defaultLightTheme = ref<any>(null);
+const defaultDarkTheme = ref<any>(null);
+const defaultLike = ref<string>('');
+const serverErrorImageUrl = ref<string | null>(null);
+const infoImageUrl = ref<string | null>(null);
+const notFoundImageUrl = ref<string | null>(null);
+const manifestJsonOverride = ref<string>('{}');
async function init() {
const meta = await os.api('admin/meta');
- iconUrl = meta.iconUrl;
- app192IconUrl = meta.app192IconUrl;
- app512IconUrl = meta.app512IconUrl;
- bannerUrl = meta.bannerUrl;
- backgroundImageUrl = meta.backgroundImageUrl;
- themeColor = meta.themeColor;
- defaultLightTheme = meta.defaultLightTheme;
- defaultDarkTheme = meta.defaultDarkTheme;
- defaultLike = meta.defaultLike;
- serverErrorImageUrl = meta.serverErrorImageUrl;
- infoImageUrl = meta.infoImageUrl;
- notFoundImageUrl = meta.notFoundImageUrl;
- manifestJsonOverride = meta.manifestJsonOverride === '' ? '{}' : JSON.stringify(JSON.parse(meta.manifestJsonOverride), null, '\t');
+ iconUrl.value = meta.iconUrl;
+ app192IconUrl.value = meta.app192IconUrl;
+ app512IconUrl.value = meta.app512IconUrl;
+ bannerUrl.value = meta.bannerUrl;
+ backgroundImageUrl.value = meta.backgroundImageUrl;
+ themeColor.value = meta.themeColor;
+ defaultLightTheme.value = meta.defaultLightTheme;
+ defaultDarkTheme.value = meta.defaultDarkTheme;
+ defaultLike.value = meta.defaultLike;
+ serverErrorImageUrl.value = meta.serverErrorImageUrl;
+ infoImageUrl.value = meta.infoImageUrl;
+ notFoundImageUrl.value = meta.notFoundImageUrl;
+ manifestJsonOverride.value = meta.manifestJsonOverride === '' ? '{}' : JSON.stringify(JSON.parse(meta.manifestJsonOverride), null, '\t');
}
function save() {
os.apiWithDialog('admin/update-meta', {
- iconUrl,
- app192IconUrl,
- app512IconUrl,
- bannerUrl,
- backgroundImageUrl,
- themeColor: themeColor === '' ? null : themeColor,
- defaultLightTheme: defaultLightTheme === '' ? null : defaultLightTheme,
- defaultDarkTheme: defaultDarkTheme === '' ? null : defaultDarkTheme,
- infoImageUrl,
- notFoundImageUrl,
- serverErrorImageUrl,
- manifestJsonOverride: manifestJsonOverride === '' ? '{}' : JSON.stringify(JSON5.parse(manifestJsonOverride)),
+ iconUrl: iconUrl.value,
+ app192IconUrl: app192IconUrl.value,
+ app512IconUrl: app512IconUrl.value,
+ bannerUrl: bannerUrl.value,
+ backgroundImageUrl: backgroundImageUrl.value,
+ themeColor: themeColor.value === '' ? null : themeColor.value,
+ defaultLightTheme: defaultLightTheme.value === '' ? null : defaultLightTheme.value,
+ defaultDarkTheme: defaultDarkTheme.value === '' ? null : defaultDarkTheme.value,
+ infoImageUrl: infoImageUrl.value,
+ notFoundImageUrl: notFoundImageUrl.value,
+ serverErrorImageUrl: serverErrorImageUrl.value,
+ manifestJsonOverride: manifestJsonOverride.value === '' ? '{}' : JSON.stringify(JSON5.parse(manifestJsonOverride.value)),
}).then(() => {
fetchInstance();
});
@@ -177,12 +174,12 @@ function chooseNewLike(ev: MouseEvent) {
defaultLike: emoji,
}).then(() => {
fetchInstance();
- defaultLike = emoji;
+ defaultLike.value = emoji as string;
});
});
}
-const headerTabs = $computed(() => []);
+const headerTabs = computed(() => []);
definePageMetadata({
title: i18n.ts.branding,
diff --git a/packages/frontend/src/pages/admin/database.vue b/packages/frontend/src/pages/admin/database.vue
index 0c14fc9b5f..d9fc672fbf 100644
--- a/packages/frontend/src/pages/admin/database.vue
+++ b/packages/frontend/src/pages/admin/database.vue
@@ -18,7 +18,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { } from 'vue';
+import { computed } from 'vue';
import FormSuspense from '@/components/form/suspense.vue';
import MkKeyValue from '@/components/MkKeyValue.vue';
import * as os from '@/os.js';
@@ -29,9 +29,9 @@ import { definePageMetadata } from '@/scripts/page-metadata.js';
const databasePromiseFactory = () => os.api('admin/get-table-stats').then(res => Object.entries(res).sort((a, b) => b[1].size - a[1].size));
-const headerActions = $computed(() => []);
+const headerActions = computed(() => []);
-const headerTabs = $computed(() => []);
+const headerTabs = computed(() => []);
definePageMetadata({
title: i18n.ts.database,
diff --git a/packages/frontend/src/pages/admin/email-settings.vue b/packages/frontend/src/pages/admin/email-settings.vue
index 8dcb5f4726..3fee3d553a 100644
--- a/packages/frontend/src/pages/admin/email-settings.vue
+++ b/packages/frontend/src/pages/admin/email-settings.vue
@@ -64,7 +64,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { } from 'vue';
+import { ref, computed } from 'vue';
import XHeader from './_header_.vue';
import MkSwitch from '@/components/MkSwitch.vue';
import MkInput from '@/components/MkInput.vue';
@@ -78,23 +78,23 @@ import { i18n } from '@/i18n.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';
import MkButton from '@/components/MkButton.vue';
-let enableEmail: boolean = $ref(false);
-let email: any = $ref(null);
-let smtpSecure: boolean = $ref(false);
-let smtpHost: string = $ref('');
-let smtpPort: number = $ref(0);
-let smtpUser: string = $ref('');
-let smtpPass: string = $ref('');
+const enableEmail = ref<boolean>(false);
+const email = ref<any>(null);
+const smtpSecure = ref<boolean>(false);
+const smtpHost = ref<string>('');
+const smtpPort = ref<number>(0);
+const smtpUser = ref<string>('');
+const smtpPass = ref<string>('');
async function init() {
const meta = await os.api('admin/meta');
- enableEmail = meta.enableEmail;
- email = meta.email;
- smtpSecure = meta.smtpSecure;
- smtpHost = meta.smtpHost;
- smtpPort = meta.smtpPort;
- smtpUser = meta.smtpUser;
- smtpPass = meta.smtpPass;
+ enableEmail.value = meta.enableEmail;
+ email.value = meta.email;
+ smtpSecure.value = meta.smtpSecure;
+ smtpHost.value = meta.smtpHost;
+ smtpPort.value = meta.smtpPort;
+ smtpUser.value = meta.smtpUser;
+ smtpPass.value = meta.smtpPass;
}
async function testEmail() {
@@ -115,19 +115,19 @@ async function testEmail() {
function save() {
os.apiWithDialog('admin/update-meta', {
- enableEmail,
- email,
- smtpSecure,
- smtpHost,
- smtpPort,
- smtpUser,
- smtpPass,
+ enableEmail: enableEmail.value,
+ email: email.value,
+ smtpSecure: smtpSecure.value,
+ smtpHost: smtpHost.value,
+ smtpPort: smtpPort.value,
+ smtpUser: smtpUser.value,
+ smtpPass: smtpPass.value,
}).then(() => {
fetchInstance();
});
}
-const headerTabs = $computed(() => []);
+const headerTabs = computed(() => []);
definePageMetadata({
title: i18n.ts.emailServer,
diff --git a/packages/frontend/src/pages/admin/external-services.vue b/packages/frontend/src/pages/admin/external-services.vue
index d1967c4bff..f4359270b6 100644
--- a/packages/frontend/src/pages/admin/external-services.vue
+++ b/packages/frontend/src/pages/admin/external-services.vue
@@ -34,10 +34,11 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { } from 'vue';
+import { ref, computed } from 'vue';
import XHeader from './_header_.vue';
import MkInput from '@/components/MkInput.vue';
import MkButton from '@/components/MkButton.vue';
+import MkSwitch from '@/components/MkSwitch.vue';
import FormSuspense from '@/components/form/suspense.vue';
import FormSection from '@/components/form/section.vue';
import * as os from '@/os.js';
@@ -45,27 +46,27 @@ import { fetchInstance } from '@/instance.js';
import { i18n } from '@/i18n.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';
-let deeplAuthKey: string = $ref('');
-let deeplIsPro: boolean = $ref(false);
+const deeplAuthKey = ref<string>('');
+const deeplIsPro = ref<boolean>(false);
async function init() {
const meta = await os.api('admin/meta');
- deeplAuthKey = meta.deeplAuthKey;
- deeplIsPro = meta.deeplIsPro;
+ deeplAuthKey.value = meta.deeplAuthKey;
+ deeplIsPro.value = meta.deeplIsPro;
}
function save() {
os.apiWithDialog('admin/update-meta', {
- deeplAuthKey,
- deeplIsPro,
+ deeplAuthKey: deeplAuthKey.value,
+ deeplIsPro: deeplIsPro.value,
}).then(() => {
fetchInstance();
});
}
-const headerActions = $computed(() => []);
+const headerActions = computed(() => []);
-const headerTabs = $computed(() => []);
+const headerTabs = computed(() => []);
definePageMetadata({
title: i18n.ts.externalServices,
diff --git a/packages/frontend/src/pages/admin/federation.vue b/packages/frontend/src/pages/admin/federation.vue
index e09d5181c5..1888a0eb16 100644
--- a/packages/frontend/src/pages/admin/federation.vue
+++ b/packages/frontend/src/pages/admin/federation.vue
@@ -59,7 +59,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { computed } from 'vue';
+import { computed, ref } from 'vue';
import XHeader from './_header_.vue';
import MkInput from '@/components/MkInput.vue';
import MkSelect from '@/components/MkSelect.vue';
@@ -69,25 +69,25 @@ import FormSplit from '@/components/form/split.vue';
import { i18n } from '@/i18n.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';
-let host = $ref('');
-let state = $ref('federating');
-let sort = $ref('+pubSub');
+const host = ref('');
+const state = ref('federating');
+const sort = ref('+pubSub');
const pagination = {
endpoint: 'federation/instances' as const,
limit: 10,
offsetMode: true,
params: computed(() => ({
- sort: sort,
- host: host !== '' ? host : null,
+ sort: sort.value,
+ host: host.value !== '' ? host.value : null,
...(
- state === 'federating' ? { federating: true } :
- state === 'subscribing' ? { subscribing: true } :
- state === 'publishing' ? { publishing: true } :
- state === 'suspended' ? { suspended: true } :
- state === 'blocked' ? { blocked: true } :
- state === 'silenced' ? { silenced: true } :
- state === 'notResponding' ? { notResponding: true } :
- state === 'nsfw' ? { nsfw: true } :
+ state.value === 'federating' ? { federating: true } :
+ state.value === 'subscribing' ? { subscribing: true } :
+ state.value === 'publishing' ? { publishing: true } :
+ state.value === 'suspended' ? { suspended: true } :
+ state.value === 'blocked' ? { blocked: true } :
+ state.value === 'silenced' ? { silenced: true } :
+ state.value === 'notResponding' ? { notResponding: true } :
+ state.value === 'nsfw' ? { nsfw: true } :
{}),
})),
};
@@ -101,9 +101,9 @@ function getStatus(instance) {
return 'Alive';
}
-const headerActions = $computed(() => []);
+const headerActions = computed(() => []);
-const headerTabs = $computed(() => []);
+const headerTabs = computed(() => []);
definePageMetadata(computed(() => ({
title: i18n.ts.federation,
diff --git a/packages/frontend/src/pages/admin/files.vue b/packages/frontend/src/pages/admin/files.vue
index 8935cd96b8..9a355865a4 100644
--- a/packages/frontend/src/pages/admin/files.vue
+++ b/packages/frontend/src/pages/admin/files.vue
@@ -36,7 +36,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { computed } from 'vue';
+import { computed, ref } from 'vue';
import XHeader from './_header_.vue';
import MkInput from '@/components/MkInput.vue';
import MkSelect from '@/components/MkSelect.vue';
@@ -45,19 +45,19 @@ import * as os from '@/os.js';
import { i18n } from '@/i18n.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';
-let origin = $ref('local');
-let type = $ref(null);
-let searchHost = $ref('');
-let userId = $ref('');
-let viewMode = $ref('grid');
+const origin = ref('local');
+const type = ref(null);
+const searchHost = ref('');
+const userId = ref('');
+const viewMode = ref('grid');
const pagination = {
endpoint: 'admin/drive/files' as const,
limit: 10,
params: computed(() => ({
- type: (type && type !== '') ? type : null,
- userId: (userId && userId !== '') ? userId : null,
- origin: origin,
- hostname: (searchHost && searchHost !== '') ? searchHost : null,
+ type: (type.value && type.value !== '') ? type.value : null,
+ userId: (userId.value && userId.value !== '') ? userId.value : null,
+ origin: origin.value,
+ hostname: (searchHost.value && searchHost.value !== '') ? searchHost.value : null,
})),
};
@@ -95,7 +95,7 @@ async function find() {
});
}
-const headerActions = $computed(() => [{
+const headerActions = computed(() => [{
text: i18n.ts.lookup,
icon: 'ph-magnifying-glass ph-bold ph-lg',
handler: find,
@@ -105,7 +105,7 @@ const headerActions = $computed(() => [{
handler: clear,
}]);
-const headerTabs = $computed(() => []);
+const headerTabs = computed(() => []);
definePageMetadata(computed(() => ({
title: i18n.ts.files,
diff --git a/packages/frontend/src/pages/admin/index.vue b/packages/frontend/src/pages/admin/index.vue
index cf62767f05..3aa74b1b91 100644
--- a/packages/frontend/src/pages/admin/index.vue
+++ b/packages/frontend/src/pages/admin/index.vue
@@ -29,7 +29,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { onActivated, onMounted, onUnmounted, provide, watch } from 'vue';
+import { onActivated, onMounted, onUnmounted, provide, watch, ref, computed } from 'vue';
import { i18n } from '@/i18n.js';
import MkSuperMenu from '@/components/MkSuperMenu.vue';
import MkInfo from '@/components/MkInfo.vue';
@@ -51,24 +51,24 @@ const indexInfo = {
provide('shouldOmitHeaderTitle', false);
-let INFO = $ref(indexInfo);
-let childInfo = $ref(null);
-let narrow = $ref(false);
-let view = $ref(null);
-let el = $ref(null);
-let pageProps = $ref({});
+const INFO = ref(indexInfo);
+const childInfo = ref(null);
+const narrow = ref(false);
+const view = ref(null);
+const el = ref(null);
+const pageProps = ref({});
let noMaintainerInformation = isEmpty(instance.maintainerName) || isEmpty(instance.maintainerEmail);
let noBotProtection = !instance.disableRegistration && !instance.enableHcaptcha && !instance.enableRecaptcha && !instance.enableTurnstile;
let noEmailServer = !instance.enableEmail;
-let thereIsUnresolvedAbuseReport = $ref(false);
-let pendingUserApprovals = $ref(false);
-let currentPage = $computed(() => router.currentRef.value.child);
+const thereIsUnresolvedAbuseReport = ref(false);
+const pendingUserApprovals = ref(false);
+const currentPage = computed(() => router.currentRef.value.child);
os.api('admin/abuse-user-reports', {
state: 'unresolved',
limit: 1,
}).then(reports => {
- if (reports.length > 0) thereIsUnresolvedAbuseReport = true;
+ if (reports.length > 0) thereIsUnresolvedAbuseReport.value = true;
});
os.api('admin/show-users', {
@@ -76,16 +76,16 @@ os.api('admin/show-users', {
origin: 'local',
limit: 1,
}).then(approvals => {
- if (approvals.length > 0) pendingUserApprovals = true;
+ if (approvals.length > 0) pendingUserApprovals.value = true;
});
const NARROW_THRESHOLD = 600;
const ro = new ResizeObserver((entries, observer) => {
if (entries.length === 0) return;
- narrow = entries[0].borderBoxSize[0].inlineSize < NARROW_THRESHOLD;
+ narrow.value = entries[0].borderBoxSize[0].inlineSize < NARROW_THRESHOLD;
});
-const menuDef = $computed(() => [{
+const menuDef = computed(() => [{
title: i18n.ts.quickAction,
items: [{
type: 'button',
@@ -104,72 +104,72 @@ const menuDef = $computed(() => [{
icon: 'ph-gauge ph-bold ph-lg',
text: i18n.ts.dashboard,
to: '/admin/overview',
- active: currentPage?.route.name === 'overview',
+ active: currentPage.value?.route.name === 'overview',
}, {
icon: 'ph-users ph-bold ph-lg',
text: i18n.ts.users,
to: '/admin/users',
- active: currentPage?.route.name === 'users',
+ active: currentPage.value?.route.name === 'users',
}, {
icon: 'ph-user-plus ph-bold ph-lg',
text: i18n.ts.invite,
to: '/admin/invites',
- active: currentPage?.route.name === 'invites',
+ active: currentPage.value?.route.name === 'invites',
}, {
icon: 'ph-chalkboard-teacher ph-bold ph-lg',
text: i18n.ts.approvals,
to: '/admin/approvals',
- active: currentPage?.route.name === 'approvals',
+ active: currentPage.value?.route.name === 'approvals',
}, {
icon: 'ph-seal-check ph-bold ph-lg',
text: i18n.ts.roles,
to: '/admin/roles',
- active: currentPage?.route.name === 'roles',
+ active: currentPage.value?.route.name === 'roles',
}, {
icon: 'ph-smiley ph-bold ph-lg',
text: i18n.ts.customEmojis,
to: '/admin/emojis',
- active: currentPage?.route.name === 'emojis',
+ active: currentPage.value?.route.name === 'emojis',
}, {
icon: 'ph-sparkle ph-bold ph-lg',
text: i18n.ts.avatarDecorations,
to: '/admin/avatar-decorations',
- active: currentPage?.route.name === 'avatarDecorations',
+ active: currentPage.value?.route.name === 'avatarDecorations',
}, {
icon: 'ph-globe-hemisphere-west ph-bold ph-lg',
text: i18n.ts.federation,
to: '/admin/federation',
- active: currentPage?.route.name === 'federation',
+ active: currentPage.value?.route.name === 'federation',
}, {
icon: 'ph-clock ph-bold ph-lg-play',
text: i18n.ts.jobQueue,
to: '/admin/queue',
- active: currentPage?.route.name === 'queue',
+ active: currentPage.value?.route.name === 'queue',
}, {
icon: 'ph-cloud ph-bold ph-lg',
text: i18n.ts.files,
to: '/admin/files',
- active: currentPage?.route.name === 'files',
+ active: currentPage.value?.route.name === 'files',
}, {
icon: 'ph-megaphone ph-bold ph-lg',
text: i18n.ts.announcements,
to: '/admin/announcements',
- active: currentPage?.route.name === 'announcements',
+ active: currentPage.value?.route.name === 'announcements',
}, {
icon: 'ph-flag ph-bold ph-lg',
text: i18n.ts.ads,
to: '/admin/ads',
- active: currentPage?.route.name === 'ads',
+ active: currentPage.value?.route.name === 'ads',
}, {
icon: 'ph-warning-circle ph-bold ph-lg',
text: i18n.ts.abuseReports,
to: '/admin/abuses',
- active: currentPage?.route.name === 'abuses',
+ active: currentPage.value?.route.name === 'abuses',
}, {
icon: 'ph-list ph-bold ph-lg-search',
text: i18n.ts.moderationLogs,
to: '/admin/modlog',
- active: currentPage?.route.name === 'modlog',
+ active: currentPage.value?.route.name === 'modlog',
}],
}, {
title: i18n.ts.settings,
@@ -177,57 +177,57 @@ const menuDef = $computed(() => [{
icon: 'ph-gear ph-bold ph-lg',
text: i18n.ts.general,
to: '/admin/settings',
- active: currentPage?.route.name === 'settings',
+ active: currentPage.value?.route.name === 'settings',
}, {
icon: 'ph-paint-roller ph-bold ph-lg',
text: i18n.ts.branding,
to: '/admin/branding',
- active: currentPage?.route.name === 'branding',
+ active: currentPage.value?.route.name === 'branding',
}, {
icon: 'ph-shield ph-bold ph-lg',
text: i18n.ts.moderation,
to: '/admin/moderation',
- active: currentPage?.route.name === 'moderation',
+ active: currentPage.value?.route.name === 'moderation',
}, {
icon: 'ph-envelope ph-bold ph-lg',
text: i18n.ts.emailServer,
to: '/admin/email-settings',
- active: currentPage?.route.name === 'email-settings',
+ active: currentPage.value?.route.name === 'email-settings',
}, {
icon: 'ph-cloud ph-bold ph-lg',
text: i18n.ts.objectStorage,
to: '/admin/object-storage',
- active: currentPage?.route.name === 'object-storage',
+ active: currentPage.value?.route.name === 'object-storage',
}, {
icon: 'ph-lock ph-bold ph-lg',
text: i18n.ts.security,
to: '/admin/security',
- active: currentPage?.route.name === 'security',
+ active: currentPage.value?.route.name === 'security',
}, {
icon: 'ph-planet ph-bold ph-lg',
text: i18n.ts.relays,
to: '/admin/relays',
- active: currentPage?.route.name === 'relays',
+ active: currentPage.value?.route.name === 'relays',
}, {
icon: 'ph-prohibit ph-bold ph-lg',
text: i18n.ts.instanceBlocking,
to: '/admin/instance-block',
- active: currentPage?.route.name === 'instance-block',
+ active: currentPage.value?.route.name === 'instance-block',
}, {
icon: 'ph-ghost ph-bold ph-lg',
text: i18n.ts.proxyAccount,
to: '/admin/proxy-account',
- active: currentPage?.route.name === 'proxy-account',
+ active: currentPage.value?.route.name === 'proxy-account',
}, {
icon: 'ph-arrow-square-out ph-bold ph-lg',
text: i18n.ts.externalServices,
to: '/admin/external-services',
- active: currentPage?.route.name === 'external-services',
+ active: currentPage.value?.route.name === 'external-services',
}, {
icon: 'ph-faders ph-bold ph-lg',
text: i18n.ts.other,
to: '/admin/other-settings',
- active: currentPage?.route.name === 'other-settings',
+ active: currentPage.value?.route.name === 'other-settings',
}],
}, {
title: i18n.ts.info,
@@ -235,28 +235,28 @@ const menuDef = $computed(() => [{
icon: 'ph-database ph-bold ph-lg',
text: i18n.ts.database,
to: '/admin/database',
- active: currentPage?.route.name === 'database',
+ active: currentPage.value?.route.name === 'database',
}],
}]);
-watch(narrow, () => {
- if (currentPage?.route.name == null && !narrow) {
+watch(narrow.value, () => {
+ if (currentPage.value?.route.name == null && !narrow.value) {
router.push('/admin/overview');
}
});
onMounted(() => {
- ro.observe(el);
+ ro.observe(el.value);
- narrow = el.offsetWidth < NARROW_THRESHOLD;
- if (currentPage?.route.name == null && !narrow) {
+ narrow.value = el.value.offsetWidth < NARROW_THRESHOLD;
+ if (currentPage.value?.route.name == null && !narrow.value) {
router.push('/admin/overview');
}
});
onActivated(() => {
- narrow = el.offsetWidth < NARROW_THRESHOLD;
- if (currentPage?.route.name == null && !narrow) {
+ narrow.value = el.value.offsetWidth < NARROW_THRESHOLD;
+ if (currentPage.value?.route.name == null && !narrow.value) {
router.push('/admin/overview');
}
});
@@ -266,16 +266,17 @@ onUnmounted(() => {
});
watch(router.currentRef, (to) => {
- if (to.route.path === '/admin' && to.child?.route.name == null && !narrow) {
+ if (to.route.path === '/admin' && to.child?.route.name == null && !narrow.value) {
router.replace('/admin/overview');
}
});
provideMetadataReceiver((info) => {
if (info == null) {
- childInfo = null;
+ childInfo.value = null;
} else {
- childInfo = info;
+ childInfo.value = info;
+ INFO.value.needWideArea = info.value.needWideArea ?? undefined;
}
});
@@ -283,7 +284,7 @@ function invite() {
os.api('admin/invite/create').then(x => {
os.alert({
type: 'info',
- text: x?.[0].code,
+ text: x[0].code,
});
}).catch(err => {
os.alert({
@@ -327,11 +328,11 @@ function lookup(ev: MouseEvent) {
}], ev.currentTarget ?? ev.target);
}
-const headerActions = $computed(() => []);
+const headerActions = computed(() => []);
-const headerTabs = $computed(() => []);
+const headerTabs = computed(() => []);
-definePageMetadata(INFO);
+definePageMetadata(INFO.value);
defineExpose({
header: {
diff --git a/packages/frontend/src/pages/admin/instance-block.vue b/packages/frontend/src/pages/admin/instance-block.vue
index 6024ed6b8f..e54f6dc065 100644
--- a/packages/frontend/src/pages/admin/instance-block.vue
+++ b/packages/frontend/src/pages/admin/instance-block.vue
@@ -23,6 +23,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
+import { ref, computed } from 'vue';
import XHeader from './_header_.vue';
import MkButton from '@/components/MkButton.vue';
import MkTextarea from '@/components/MkTextarea.vue';
@@ -32,29 +33,29 @@ import { fetchInstance } from '@/instance.js';
import { i18n } from '@/i18n.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';
-let blockedHosts: string = $ref('');
-let silencedHosts: string = $ref('');
-let tab = $ref('block');
+const blockedHosts = ref<string>('');
+const silencedHosts = ref<string>('');
+const tab = ref('block');
async function init() {
const meta = await os.api('admin/meta');
- blockedHosts = meta.blockedHosts.join('\n');
- silencedHosts = meta.silencedHosts.join('\n');
+ blockedHosts.value = meta.blockedHosts.join('\n');
+ silencedHosts.value = meta.silencedHosts.join('\n');
}
function save() {
os.apiWithDialog('admin/update-meta', {
- blockedHosts: blockedHosts.split('\n') || [],
- silencedHosts: silencedHosts.split('\n') || [],
+ blockedHosts: blockedHosts.value.split('\n') || [],
+ silencedHosts: silencedHosts.value.split('\n') || [],
}).then(() => {
fetchInstance();
});
}
-const headerActions = $computed(() => []);
+const headerActions = computed(() => []);
-const headerTabs = $computed(() => [{
+const headerTabs = computed(() => [{
key: 'block',
title: i18n.ts.block,
icon: 'ph-prohibit ph-bold ph-lg',
diff --git a/packages/frontend/src/pages/admin/invites.vue b/packages/frontend/src/pages/admin/invites.vue
index 406317f660..6314d0ce4e 100644
--- a/packages/frontend/src/pages/admin/invites.vue
+++ b/packages/frontend/src/pages/admin/invites.vue
@@ -70,8 +70,8 @@ import { definePageMetadata } from '@/scripts/page-metadata.js';
const pagingComponent = shallowRef<InstanceType<typeof MkPagination>>();
-let type = ref('all');
-let sort = ref('+createdAt');
+const type = ref('all');
+const sort = ref('+createdAt');
const pagination: Paging = {
endpoint: 'admin/invite/list' as const,
@@ -109,8 +109,8 @@ function deleted(id: string) {
}
}
-const headerActions = $computed(() => []);
-const headerTabs = $computed(() => []);
+const headerActions = computed(() => []);
+const headerTabs = computed(() => []);
definePageMetadata({
title: i18n.ts.invite,
diff --git a/packages/frontend/src/pages/admin/moderation.vue b/packages/frontend/src/pages/admin/moderation.vue
index cacb3254a8..9539611f76 100644
--- a/packages/frontend/src/pages/admin/moderation.vue
+++ b/packages/frontend/src/pages/admin/moderation.vue
@@ -48,6 +48,11 @@ SPDX-License-Identifier: AGPL-3.0-only
<template #label>{{ i18n.ts.sensitiveWords }}</template>
<template #caption>{{ i18n.ts.sensitiveWordsDescription }}<br>{{ i18n.ts.sensitiveWordsDescription2 }}</template>
</MkTextarea>
+
+ <MkTextarea v-model="hiddenTags">
+ <template #label>{{ i18n.ts.hiddenTags }}</template>
+ <template #caption>{{ i18n.ts.hiddenTagsDescription }}</template>
+ </MkTextarea>
</div>
</FormSuspense>
</MkSpacer>
@@ -63,13 +68,11 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { } from 'vue';
+import { ref, computed } from 'vue';
import XHeader from './_header_.vue';
import MkSwitch from '@/components/MkSwitch.vue';
import MkInput from '@/components/MkInput.vue';
import MkTextarea from '@/components/MkTextarea.vue';
-import FormSection from '@/components/form/section.vue';
-import FormSplit from '@/components/form/split.vue';
import FormSuspense from '@/components/form/suspense.vue';
import * as os from '@/os.js';
import { fetchInstance } from '@/instance.js';
@@ -78,45 +81,48 @@ import { definePageMetadata } from '@/scripts/page-metadata.js';
import MkButton from '@/components/MkButton.vue';
import FormLink from '@/components/form/link.vue';
-let enableRegistration: boolean = $ref(false);
-let emailRequiredForSignup: boolean = $ref(false);
-let approvalRequiredForSignup: boolean = $ref(false);
-let bubbleTimelineEnabled: boolean = $ref(false);
-let sensitiveWords: string = $ref('');
-let preservedUsernames: string = $ref('');
-let bubbleTimeline: string = $ref('');
-let tosUrl: string | null = $ref(null);
-let privacyPolicyUrl: string | null = $ref(null);
+const enableRegistration = ref<boolean>(false);
+const emailRequiredForSignup = ref<boolean>(false);
+const approvalRequiredForSignup = ref<boolean>(false);
+const bubbleTimelineEnabled = ref<boolean>(false);
+const sensitiveWords = ref<string>('');
+const hiddenTags = ref<string>('');
+const preservedUsernames = ref<string>('');
+const bubbleTimeline = ref<string>('');
+const tosUrl = ref<string | null>(null);
+const privacyPolicyUrl = ref<string | null>(null);
async function init() {
const meta = await os.api('admin/meta');
- enableRegistration = !meta.disableRegistration;
- emailRequiredForSignup = meta.emailRequiredForSignup;
- approvalRequiredForSignup = meta.approvalRequiredForSignup;
- sensitiveWords = meta.sensitiveWords.join('\n');
- preservedUsernames = meta.preservedUsernames.join('\n');
- tosUrl = meta.tosUrl;
- privacyPolicyUrl = meta.privacyPolicyUrl;
- bubbleTimeline = meta.bubbleInstances.join('\n');
- bubbleTimelineEnabled = meta.policies.btlAvailable;
+ enableRegistration.value = !meta.disableRegistration;
+ emailRequiredForSignup.value = meta.emailRequiredForSignup;
+ approvalRequiredForSignup.value = meta.approvalRequiredForSignup;
+ sensitiveWords.value = meta.sensitiveWords.join('\n');
+ hiddenTags.value = meta.hiddenTags.join('\n');
+ preservedUsernames.value = meta.preservedUsernames.join('\n');
+ tosUrl.value = meta.tosUrl;
+ privacyPolicyUrl.value = meta.privacyPolicyUrl;
+ bubbleTimeline.value = meta.bubbleInstances.join('\n');
+ bubbleTimelineEnabled.value = meta.policies.btlAvailable;
}
function save() {
os.apiWithDialog('admin/update-meta', {
- disableRegistration: !enableRegistration,
- emailRequiredForSignup,
- approvalRequiredForSignup,
- tosUrl,
- privacyPolicyUrl,
- sensitiveWords: sensitiveWords.split('\n'),
- preservedUsernames: preservedUsernames.split('\n'),
- bubbleInstances: bubbleTimeline.split('\n'),
+ disableRegistration: !enableRegistration.value,
+ emailRequiredForSignup: emailRequiredForSignup.value,
+ approvalRequiredForSignup: approvalRequiredForSignup.value,
+ tosUrl: tosUrl.value,
+ privacyPolicyUrl: privacyPolicyUrl.value,
+ sensitiveWords: sensitiveWords.value.split('\n'),
+ hiddenTags: hiddenTags.value.split('\n'),
+ preservedUsernames: preservedUsernames.value.split('\n'),
+ bubbleInstances: bubbleTimeline.value.split('\n'),
}).then(() => {
fetchInstance();
});
}
-const headerTabs = $computed(() => []);
+const headerTabs = computed(() => []);
definePageMetadata({
title: i18n.ts.moderation,
diff --git a/packages/frontend/src/pages/admin/modlog.ModLog.vue b/packages/frontend/src/pages/admin/modlog.ModLog.vue
index a6612f17d1..4436fbac89 100644
--- a/packages/frontend/src/pages/admin/modlog.ModLog.vue
+++ b/packages/frontend/src/pages/admin/modlog.ModLog.vue
@@ -49,7 +49,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkTime :time="log.createdAt"/>
</template>
- <div :class="$style.root">
+ <div>
<div style="display: flex; gap: var(--margin); flex-wrap: wrap;">
<div style="flex: 1;">{{ i18n.ts.moderator }}: <MkA :to="`/admin/user/${log.userId}`" class="_link">@{{ log.user?.username }}</MkA></div>
<div style="flex: 1;">{{ i18n.ts.dateAndTime }}: <MkTime :time="log.createdAt" mode="detail"/></div>
@@ -127,9 +127,7 @@ SPDX-License-Identifier: AGPL-3.0-only
import * as Misskey from 'misskey-js';
import { CodeDiff } from 'v-code-diff';
import JSON5 from 'json5';
-import * as os from '@/os.js';
import { i18n } from '@/i18n.js';
-import { dateString } from '@/filters/date.js';
import MkFolder from '@/components/MkFolder.vue';
const props = defineProps<{
@@ -138,9 +136,6 @@ const props = defineProps<{
</script>
<style lang="scss" module>
-.root {
-}
-
.avatar {
width: 18px;
height: 18px;
diff --git a/packages/frontend/src/pages/admin/modlog.vue b/packages/frontend/src/pages/admin/modlog.vue
index bdbed67ad1..a50d401ba2 100644
--- a/packages/frontend/src/pages/admin/modlog.vue
+++ b/packages/frontend/src/pages/admin/modlog.vue
@@ -30,7 +30,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { computed } from 'vue';
+import { computed, shallowRef, ref } from 'vue';
import * as Misskey from 'misskey-js';
import XHeader from './_header_.vue';
import XModLog from './modlog.ModLog.vue';
@@ -40,25 +40,25 @@ import MkPagination from '@/components/MkPagination.vue';
import { i18n } from '@/i18n.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';
-let logs = $shallowRef<InstanceType<typeof MkPagination>>();
+const logs = shallowRef<InstanceType<typeof MkPagination>>();
-let type = $ref(null);
-let moderatorId = $ref('');
+const type = ref(null);
+const moderatorId = ref('');
const pagination = {
endpoint: 'admin/show-moderation-logs' as const,
limit: 30,
params: computed(() => ({
- type,
- userId: moderatorId === '' ? null : moderatorId,
+ type: type.value,
+ userId: moderatorId.value === '' ? null : moderatorId.value,
})),
};
console.log(Misskey);
-const headerActions = $computed(() => []);
+const headerActions = computed(() => []);
-const headerTabs = $computed(() => []);
+const headerTabs = computed(() => []);
definePageMetadata({
title: i18n.ts.moderationLogs,
diff --git a/packages/frontend/src/pages/admin/object-storage.vue b/packages/frontend/src/pages/admin/object-storage.vue
index cd1f4ca601..e71e53c942 100644
--- a/packages/frontend/src/pages/admin/object-storage.vue
+++ b/packages/frontend/src/pages/admin/object-storage.vue
@@ -83,7 +83,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { } from 'vue';
+import { ref, computed } from 'vue';
import XHeader from './_header_.vue';
import MkSwitch from '@/components/MkSwitch.vue';
import MkInput from '@/components/MkInput.vue';
@@ -95,58 +95,58 @@ import { i18n } from '@/i18n.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';
import MkButton from '@/components/MkButton.vue';
-let useObjectStorage: boolean = $ref(false);
-let objectStorageBaseUrl: string | null = $ref(null);
-let objectStorageBucket: string | null = $ref(null);
-let objectStoragePrefix: string | null = $ref(null);
-let objectStorageEndpoint: string | null = $ref(null);
-let objectStorageRegion: string | null = $ref(null);
-let objectStoragePort: number | null = $ref(null);
-let objectStorageAccessKey: string | null = $ref(null);
-let objectStorageSecretKey: string | null = $ref(null);
-let objectStorageUseSSL: boolean = $ref(false);
-let objectStorageUseProxy: boolean = $ref(false);
-let objectStorageSetPublicRead: boolean = $ref(false);
-let objectStorageS3ForcePathStyle: boolean = $ref(true);
+const useObjectStorage = ref<boolean>(false);
+const objectStorageBaseUrl = ref<string | null>(null);
+const objectStorageBucket = ref<string | null>(null);
+const objectStoragePrefix = ref<string | null>(null);
+const objectStorageEndpoint = ref<string | null>(null);
+const objectStorageRegion = ref<string | null>(null);
+const objectStoragePort = ref<number | null>(null);
+const objectStorageAccessKey = ref<string | null>(null);
+const objectStorageSecretKey = ref<string | null>(null);
+const objectStorageUseSSL = ref<boolean>(false);
+const objectStorageUseProxy = ref<boolean>(false);
+const objectStorageSetPublicRead = ref<boolean>(false);
+const objectStorageS3ForcePathStyle = ref<boolean>(true);
async function init() {
const meta = await os.api('admin/meta');
- useObjectStorage = meta.useObjectStorage;
- objectStorageBaseUrl = meta.objectStorageBaseUrl;
- objectStorageBucket = meta.objectStorageBucket;
- objectStoragePrefix = meta.objectStoragePrefix;
- objectStorageEndpoint = meta.objectStorageEndpoint;
- objectStorageRegion = meta.objectStorageRegion;
- objectStoragePort = meta.objectStoragePort;
- objectStorageAccessKey = meta.objectStorageAccessKey;
- objectStorageSecretKey = meta.objectStorageSecretKey;
- objectStorageUseSSL = meta.objectStorageUseSSL;
- objectStorageUseProxy = meta.objectStorageUseProxy;
- objectStorageSetPublicRead = meta.objectStorageSetPublicRead;
- objectStorageS3ForcePathStyle = meta.objectStorageS3ForcePathStyle;
+ useObjectStorage.value = meta.useObjectStorage;
+ objectStorageBaseUrl.value = meta.objectStorageBaseUrl;
+ objectStorageBucket.value = meta.objectStorageBucket;
+ objectStoragePrefix.value = meta.objectStoragePrefix;
+ objectStorageEndpoint.value = meta.objectStorageEndpoint;
+ objectStorageRegion.value = meta.objectStorageRegion;
+ objectStoragePort.value = meta.objectStoragePort;
+ objectStorageAccessKey.value = meta.objectStorageAccessKey;
+ objectStorageSecretKey.value = meta.objectStorageSecretKey;
+ objectStorageUseSSL.value = meta.objectStorageUseSSL;
+ objectStorageUseProxy.value = meta.objectStorageUseProxy;
+ objectStorageSetPublicRead.value = meta.objectStorageSetPublicRead;
+ objectStorageS3ForcePathStyle.value = meta.objectStorageS3ForcePathStyle;
}
function save() {
os.apiWithDialog('admin/update-meta', {
- useObjectStorage,
- objectStorageBaseUrl,
- objectStorageBucket,
- objectStoragePrefix,
- objectStorageEndpoint,
- objectStorageRegion,
- objectStoragePort,
- objectStorageAccessKey,
- objectStorageSecretKey,
- objectStorageUseSSL,
- objectStorageUseProxy,
- objectStorageSetPublicRead,
- objectStorageS3ForcePathStyle,
+ useObjectStorage: useObjectStorage.value,
+ objectStorageBaseUrl: objectStorageBaseUrl.value,
+ objectStorageBucket: objectStorageBucket.value,
+ objectStoragePrefix: objectStoragePrefix.value,
+ objectStorageEndpoint: objectStorageEndpoint.value,
+ objectStorageRegion: objectStorageRegion.value,
+ objectStoragePort: objectStoragePort.value,
+ objectStorageAccessKey: objectStorageAccessKey.value,
+ objectStorageSecretKey: objectStorageSecretKey.value,
+ objectStorageUseSSL: objectStorageUseSSL.value,
+ objectStorageUseProxy: objectStorageUseProxy.value,
+ objectStorageSetPublicRead: objectStorageSetPublicRead.value,
+ objectStorageS3ForcePathStyle: objectStorageS3ForcePathStyle.value,
}).then(() => {
fetchInstance();
});
}
-const headerTabs = $computed(() => []);
+const headerTabs = computed(() => []);
definePageMetadata({
title: i18n.ts.objectStorage,
diff --git a/packages/frontend/src/pages/admin/other-settings.vue b/packages/frontend/src/pages/admin/other-settings.vue
index d1bc420db8..6523676a18 100644
--- a/packages/frontend/src/pages/admin/other-settings.vue
+++ b/packages/frontend/src/pages/admin/other-settings.vue
@@ -57,7 +57,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { } from 'vue';
+import { ref, computed } from 'vue';
import XHeader from './_header_.vue';
import FormSuspense from '@/components/form/suspense.vue';
import * as os from '@/os.js';
@@ -66,44 +66,44 @@ import { i18n } from '@/i18n.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';
import MkSwitch from '@/components/MkSwitch.vue';
-let enableServerMachineStats: boolean = $ref(false);
-let enableAchievements: boolean = $ref(false);
-let enableBotTrending: boolean = $ref(false);
-let enableIdenticonGeneration: boolean = $ref(false);
-let enableChartsForRemoteUser: boolean = $ref(false);
-let enableChartsForFederatedInstances: boolean = $ref(false);
+const enableServerMachineStats = ref<boolean>(false);
+const enableAchievements = ref<boolean>(false);
+const enableBotTrending = ref<boolean>(false);
+const enableIdenticonGeneration = ref<boolean>(false);
+const enableChartsForRemoteUser = ref<boolean>(false);
+const enableChartsForFederatedInstances = ref<boolean>(false);
async function init() {
const meta = await os.api('admin/meta');
- enableServerMachineStats = meta.enableServerMachineStats;
- enableAchievements = meta.enableAchievements;
- enableBotTrending = meta.enableBotTrending;
- enableIdenticonGeneration = meta.enableIdenticonGeneration;
- enableChartsForRemoteUser = meta.enableChartsForRemoteUser;
- enableChartsForFederatedInstances = meta.enableChartsForFederatedInstances;
+ enableServerMachineStats.value = meta.enableServerMachineStats;
+ enableAchievements.value = meta.enableAchievements;
+ enableBotTrending.value = meta.enableBotTrending;
+ enableIdenticonGeneration.value = meta.enableIdenticonGeneration;
+ enableChartsForRemoteUser.value = meta.enableChartsForRemoteUser;
+ enableChartsForFederatedInstances.value = meta.enableChartsForFederatedInstances;
}
function save() {
os.apiWithDialog('admin/update-meta', {
- enableServerMachineStats,
- enableAchievements,
- enableBotTrending,
- enableIdenticonGeneration,
- enableChartsForRemoteUser,
- enableChartsForFederatedInstances,
+ enableServerMachineStats: enableServerMachineStats.value,
+ enableAchievements: enableAchievements.value,
+ enableBotTrending: enableBotTrending.value,
+ enableIdenticonGeneration: enableIdenticonGeneration.value,
+ enableChartsForRemoteUser: enableChartsForRemoteUser.value,
+ enableChartsForFederatedInstances: enableChartsForFederatedInstances.value,
}).then(() => {
fetchInstance();
});
}
-const headerActions = $computed(() => [{
+const headerActions = computed(() => [{
asFullButton: true,
icon: 'ph-check ph-bold ph-lg',
text: i18n.ts.save,
handler: save,
}]);
-const headerTabs = $computed(() => []);
+const headerTabs = computed(() => []);
definePageMetadata({
title: i18n.ts.other,
diff --git a/packages/frontend/src/pages/admin/overview.active-users.vue b/packages/frontend/src/pages/admin/overview.active-users.vue
index 8426c463d2..5e67370c2b 100644
--- a/packages/frontend/src/pages/admin/overview.active-users.vue
+++ b/packages/frontend/src/pages/admin/overview.active-users.vue
@@ -13,7 +13,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { onMounted } from 'vue';
+import { onMounted, shallowRef, ref } from 'vue';
import { Chart } from 'chart.js';
import gradient from 'chartjs-plugin-gradient';
import * as os from '@/os.js';
@@ -24,11 +24,11 @@ import { initChart } from '@/scripts/init-chart.js';
initChart();
-const chartEl = $shallowRef<HTMLCanvasElement>(null);
+const chartEl = shallowRef<HTMLCanvasElement>(null);
const now = new Date();
let chartInstance: Chart = null;
const chartLimit = 7;
-let fetching = $ref(true);
+const fetching = ref(true);
const { handler: externalTooltipHandler } = useChartTooltip();
@@ -61,7 +61,7 @@ async function renderChart() {
const max = Math.max(...raw.read);
- chartInstance = new Chart(chartEl, {
+ chartInstance = new Chart(chartEl.value, {
type: 'bar',
data: {
datasets: [{
@@ -155,7 +155,7 @@ async function renderChart() {
plugins: [chartVLine(vLineColor)],
});
- fetching = false;
+ fetching.value = false;
}
onMounted(async () => {
diff --git a/packages/frontend/src/pages/admin/overview.ap-requests.vue b/packages/frontend/src/pages/admin/overview.ap-requests.vue
index cd54041c34..0de62fadea 100644
--- a/packages/frontend/src/pages/admin/overview.ap-requests.vue
+++ b/packages/frontend/src/pages/admin/overview.ap-requests.vue
@@ -20,7 +20,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { onMounted } from 'vue';
+import { onMounted, shallowRef, ref } from 'vue';
import { Chart } from 'chart.js';
import gradient from 'chartjs-plugin-gradient';
import * as os from '@/os.js';
@@ -33,9 +33,9 @@ import { initChart } from '@/scripts/init-chart.js';
initChart();
const chartLimit = 50;
-const chartEl = $shallowRef<HTMLCanvasElement>();
-const chartEl2 = $shallowRef<HTMLCanvasElement>();
-let fetching = $ref(true);
+const chartEl = shallowRef<HTMLCanvasElement>();
+const chartEl2 = shallowRef<HTMLCanvasElement>();
+const fetching = ref(true);
const { handler: externalTooltipHandler } = useChartTooltip();
const { handler: externalTooltipHandler2 } = useChartTooltip();
@@ -74,7 +74,7 @@ onMounted(async () => {
const succMax = Math.max(...raw.deliverSucceeded);
const failMax = Math.max(...raw.deliverFailed);
- new Chart(chartEl, {
+ new Chart(chartEl.value, {
type: 'line',
data: {
datasets: [{
@@ -178,7 +178,7 @@ onMounted(async () => {
plugins: [chartVLine(vLineColor)],
});
- new Chart(chartEl2, {
+ new Chart(chartEl2.value, {
type: 'bar',
data: {
datasets: [{
@@ -265,7 +265,7 @@ onMounted(async () => {
plugins: [chartVLine(vLineColor)],
});
- fetching = false;
+ fetching.value = false;
});
</script>
diff --git a/packages/frontend/src/pages/admin/overview.federation.vue b/packages/frontend/src/pages/admin/overview.federation.vue
index 33dbbd0d84..03e33e57c4 100644
--- a/packages/frontend/src/pages/admin/overview.federation.vue
+++ b/packages/frontend/src/pages/admin/overview.federation.vue
@@ -46,7 +46,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { onMounted } from 'vue';
+import { onMounted, ref } from 'vue';
import XPie from './overview.pie.vue';
import * as os from '@/os.js';
import number from '@/filters/number.js';
@@ -54,25 +54,25 @@ import MkNumberDiff from '@/components/MkNumberDiff.vue';
import { i18n } from '@/i18n.js';
import { useChartTooltip } from '@/scripts/use-chart-tooltip.js';
-let topSubInstancesForPie: any = $ref(null);
-let topPubInstancesForPie: any = $ref(null);
-let federationPubActive = $ref<number | null>(null);
-let federationPubActiveDiff = $ref<number | null>(null);
-let federationSubActive = $ref<number | null>(null);
-let federationSubActiveDiff = $ref<number | null>(null);
-let fetching = $ref(true);
+const topSubInstancesForPie = ref<any>(null);
+const topPubInstancesForPie = ref<any>(null);
+const federationPubActive = ref<number | null>(null);
+const federationPubActiveDiff = ref<number | null>(null);
+const federationSubActive = ref<number | null>(null);
+const federationSubActiveDiff = ref<number | null>(null);
+const fetching = ref(true);
const { handler: externalTooltipHandler } = useChartTooltip();
onMounted(async () => {
const chart = await os.apiGet('charts/federation', { limit: 2, span: 'day' });
- federationPubActive = chart.pubActive[0];
- federationPubActiveDiff = chart.pubActive[0] - chart.pubActive[1];
- federationSubActive = chart.subActive[0];
- federationSubActiveDiff = chart.subActive[0] - chart.subActive[1];
+ federationPubActive.value = chart.pubActive[0];
+ federationPubActiveDiff.value = chart.pubActive[0] - chart.pubActive[1];
+ federationSubActive.value = chart.subActive[0];
+ federationSubActiveDiff.value = chart.subActive[0] - chart.subActive[1];
os.apiGet('federation/stats', { limit: 10 }).then(res => {
- topSubInstancesForPie = res.topSubInstances.map(x => ({
+ topSubInstancesForPie.value = res.topSubInstances.map(x => ({
name: x.host,
color: x.themeColor,
value: x.followersCount,
@@ -80,7 +80,7 @@ onMounted(async () => {
os.pageWindow(`/instance-info/${x.host}`);
},
})).concat([{ name: '(other)', color: '#80808080', value: res.otherFollowersCount }]);
- topPubInstancesForPie = res.topPubInstances.map(x => ({
+ topPubInstancesForPie.value = res.topPubInstances.map(x => ({
name: x.host,
color: x.themeColor,
value: x.followingCount,
@@ -90,7 +90,7 @@ onMounted(async () => {
})).concat([{ name: '(other)', color: '#80808080', value: res.otherFollowingCount }]);
});
- fetching = false;
+ fetching.value = false;
});
</script>
diff --git a/packages/frontend/src/pages/admin/overview.heatmap.vue b/packages/frontend/src/pages/admin/overview.heatmap.vue
index 4d09f183bf..8e3c809353 100644
--- a/packages/frontend/src/pages/admin/overview.heatmap.vue
+++ b/packages/frontend/src/pages/admin/overview.heatmap.vue
@@ -17,10 +17,11 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
+import { ref } from 'vue';
import MkHeatmap from '@/components/MkHeatmap.vue';
import MkSelect from '@/components/MkSelect.vue';
-let src = $ref('active-users');
+const src = ref('active-users');
</script>
<style lang="scss" module>
diff --git a/packages/frontend/src/pages/admin/overview.moderators.vue b/packages/frontend/src/pages/admin/overview.moderators.vue
index 4086ca51f0..c6e81b4a18 100644
--- a/packages/frontend/src/pages/admin/overview.moderators.vue
+++ b/packages/frontend/src/pages/admin/overview.moderators.vue
@@ -17,21 +17,21 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { onMounted } from 'vue';
+import { onMounted, ref } from 'vue';
import * as os from '@/os.js';
import { defaultStore } from '@/store.js';
-let moderators: any = $ref(null);
-let fetching = $ref(true);
+const moderators = ref<any>(null);
+const fetching = ref(true);
onMounted(async () => {
- moderators = await os.api('admin/show-users', {
+ moderators.value = await os.api('admin/show-users', {
sort: '+lastActiveDate',
state: 'adminOrModerator',
limit: 30,
});
- fetching = false;
+ fetching.value = false;
});
</script>
diff --git a/packages/frontend/src/pages/admin/overview.queue.vue b/packages/frontend/src/pages/admin/overview.queue.vue
index 1af9d89f62..b6b3bf194a 100644
--- a/packages/frontend/src/pages/admin/overview.queue.vue
+++ b/packages/frontend/src/pages/admin/overview.queue.vue
@@ -35,7 +35,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { markRaw, onMounted, onUnmounted, ref } from 'vue';
+import { markRaw, onMounted, onUnmounted, ref, shallowRef } from 'vue';
import XChart from './overview.queue.chart.vue';
import number from '@/filters/number.js';
import { useStream } from '@/stream.js';
@@ -46,10 +46,10 @@ const activeSincePrevTick = ref(0);
const active = ref(0);
const delayed = ref(0);
const waiting = ref(0);
-let chartProcess = $shallowRef<InstanceType<typeof XChart>>();
-let chartActive = $shallowRef<InstanceType<typeof XChart>>();
-let chartDelayed = $shallowRef<InstanceType<typeof XChart>>();
-let chartWaiting = $shallowRef<InstanceType<typeof XChart>>();
+const chartProcess = shallowRef<InstanceType<typeof XChart>>();
+const chartActive = shallowRef<InstanceType<typeof XChart>>();
+const chartDelayed = shallowRef<InstanceType<typeof XChart>>();
+const chartWaiting = shallowRef<InstanceType<typeof XChart>>();
const props = defineProps<{
domain: string;
@@ -61,10 +61,10 @@ const onStats = (stats) => {
delayed.value = stats[props.domain].delayed;
waiting.value = stats[props.domain].waiting;
- chartProcess.pushData(stats[props.domain].activeSincePrevTick);
- chartActive.pushData(stats[props.domain].active);
- chartDelayed.pushData(stats[props.domain].delayed);
- chartWaiting.pushData(stats[props.domain].waiting);
+ chartProcess.value.pushData(stats[props.domain].activeSincePrevTick);
+ chartActive.value.pushData(stats[props.domain].active);
+ chartDelayed.value.pushData(stats[props.domain].delayed);
+ chartWaiting.value.pushData(stats[props.domain].waiting);
};
const onStatsLog = (statsLog) => {
@@ -80,10 +80,10 @@ const onStatsLog = (statsLog) => {
dataWaiting.push(stats[props.domain].waiting);
}
- chartProcess.setData(dataProcess);
- chartActive.setData(dataActive);
- chartDelayed.setData(dataDelayed);
- chartWaiting.setData(dataWaiting);
+ chartProcess.value.setData(dataProcess);
+ chartActive.value.setData(dataActive);
+ chartDelayed.value.setData(dataDelayed);
+ chartWaiting.value.setData(dataWaiting);
};
onMounted(() => {
diff --git a/packages/frontend/src/pages/admin/overview.stats.vue b/packages/frontend/src/pages/admin/overview.stats.vue
index 23ed736c94..b853aee55d 100644
--- a/packages/frontend/src/pages/admin/overview.stats.vue
+++ b/packages/frontend/src/pages/admin/overview.stats.vue
@@ -61,7 +61,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { onMounted } from 'vue';
+import { onMounted, ref } from 'vue';
import * as os from '@/os.js';
import MkNumberDiff from '@/components/MkNumberDiff.vue';
import MkNumber from '@/components/MkNumber.vue';
@@ -69,29 +69,29 @@ import { i18n } from '@/i18n.js';
import { customEmojis } from '@/custom-emojis.js';
import { defaultStore } from '@/store.js';
-let stats: any = $ref(null);
-let usersComparedToThePrevDay = $ref<number>();
-let notesComparedToThePrevDay = $ref<number>();
-let onlineUsersCount = $ref(0);
-let fetching = $ref(true);
+const stats = ref<any>(null);
+const usersComparedToThePrevDay = ref<number>();
+const notesComparedToThePrevDay = ref<number>();
+const onlineUsersCount = ref(0);
+const fetching = ref(true);
onMounted(async () => {
const [_stats, _onlineUsersCount] = await Promise.all([
os.api('stats', {}),
os.apiGet('get-online-users-count').then(res => res.count),
]);
- stats = _stats;
- onlineUsersCount = _onlineUsersCount;
+ stats.value = _stats;
+ onlineUsersCount.value = _onlineUsersCount;
os.apiGet('charts/users', { limit: 2, span: 'day' }).then(chart => {
- usersComparedToThePrevDay = stats.originalUsersCount - chart.local.total[1];
+ usersComparedToThePrevDay.value = stats.value.originalUsersCount - chart.local.total[1];
});
os.apiGet('charts/notes', { limit: 2, span: 'day' }).then(chart => {
- notesComparedToThePrevDay = stats.originalNotesCount - chart.local.total[1];
+ notesComparedToThePrevDay.value = stats.value.originalNotesCount - chart.local.total[1];
});
- fetching = false;
+ fetching.value = false;
});
</script>
diff --git a/packages/frontend/src/pages/admin/overview.users.vue b/packages/frontend/src/pages/admin/overview.users.vue
index 6ee83c51e7..6b8dd90747 100644
--- a/packages/frontend/src/pages/admin/overview.users.vue
+++ b/packages/frontend/src/pages/admin/overview.users.vue
@@ -17,13 +17,14 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
+import { ref } from 'vue';
import * as os from '@/os.js';
import { useInterval } from '@/scripts/use-interval.js';
import MkUserCardMini from '@/components/MkUserCardMini.vue';
import { defaultStore } from '@/store.js';
-let newUsers = $ref(null);
-let fetching = $ref(true);
+const newUsers = ref(null);
+const fetching = ref(true);
const fetch = async () => {
const _newUsers = await os.api('admin/show-users', {
@@ -31,8 +32,8 @@ const fetch = async () => {
sort: '+createdAt',
origin: 'local',
});
- newUsers = _newUsers;
- fetching = false;
+ newUsers.value = _newUsers;
+ fetching.value = false;
};
useInterval(fetch, 1000 * 60, {
diff --git a/packages/frontend/src/pages/admin/overview.vue b/packages/frontend/src/pages/admin/overview.vue
index 7563ae9834..edeab30cbb 100644
--- a/packages/frontend/src/pages/admin/overview.vue
+++ b/packages/frontend/src/pages/admin/overview.vue
@@ -65,7 +65,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { markRaw, onMounted, onBeforeUnmount, nextTick } from 'vue';
+import { markRaw, onMounted, onBeforeUnmount, nextTick, shallowRef, ref, computed } from 'vue';
import XFederation from './overview.federation.vue';
import XInstances from './overview.instances.vue';
import XQueue from './overview.queue.vue';
@@ -82,16 +82,16 @@ import { i18n } from '@/i18n.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';
import MkFoldableSection from '@/components/MkFoldableSection.vue';
-const rootEl = $shallowRef<HTMLElement>();
-let serverInfo: any = $ref(null);
-let topSubInstancesForPie: any = $ref(null);
-let topPubInstancesForPie: any = $ref(null);
-let federationPubActive = $ref<number | null>(null);
-let federationPubActiveDiff = $ref<number | null>(null);
-let federationSubActive = $ref<number | null>(null);
-let federationSubActiveDiff = $ref<number | null>(null);
-let newUsers = $ref(null);
-let activeInstances = $shallowRef(null);
+const rootEl = shallowRef<HTMLElement>();
+const serverInfo = ref<any>(null);
+const topSubInstancesForPie = ref<any>(null);
+const topPubInstancesForPie = ref<any>(null);
+const federationPubActive = ref<number | null>(null);
+const federationPubActiveDiff = ref<number | null>(null);
+const federationSubActive = ref<number | null>(null);
+const federationSubActiveDiff = ref<number | null>(null);
+const newUsers = ref(null);
+const activeInstances = shallowRef(null);
const queueStatsConnection = markRaw(useStream().useChannel('queueStats'));
const now = new Date();
const filesPagination = {
@@ -116,14 +116,14 @@ onMounted(async () => {
*/
os.apiGet('charts/federation', { limit: 2, span: 'day' }).then(chart => {
- federationPubActive = chart.pubActive[0];
- federationPubActiveDiff = chart.pubActive[0] - chart.pubActive[1];
- federationSubActive = chart.subActive[0];
- federationSubActiveDiff = chart.subActive[0] - chart.subActive[1];
+ federationPubActive.value = chart.pubActive[0];
+ federationPubActiveDiff.value = chart.pubActive[0] - chart.pubActive[1];
+ federationSubActive.value = chart.subActive[0];
+ federationSubActiveDiff.value = chart.subActive[0] - chart.subActive[1];
});
os.apiGet('federation/stats', { limit: 10 }).then(res => {
- topSubInstancesForPie = res.topSubInstances.map(x => ({
+ topSubInstancesForPie.value = res.topSubInstances.map(x => ({
name: x.host,
color: x.themeColor,
value: x.followersCount,
@@ -131,7 +131,7 @@ onMounted(async () => {
os.pageWindow(`/instance-info/${x.host}`);
},
})).concat([{ name: '(other)', color: '#80808080', value: res.otherFollowersCount }]);
- topPubInstancesForPie = res.topPubInstances.map(x => ({
+ topPubInstancesForPie.value = res.topPubInstances.map(x => ({
name: x.host,
color: x.themeColor,
value: x.followingCount,
@@ -142,21 +142,21 @@ onMounted(async () => {
});
os.api('admin/server-info').then(serverInfoResponse => {
- serverInfo = serverInfoResponse;
+ serverInfo.value = serverInfoResponse;
});
os.api('admin/show-users', {
limit: 5,
sort: '+createdAt',
}).then(res => {
- newUsers = res;
+ newUsers.value = res;
});
os.api('federation/instances', {
sort: '+latestRequestReceivedAt',
limit: 25,
}).then(res => {
- activeInstances = res;
+ activeInstances.value = res;
});
nextTick(() => {
@@ -171,9 +171,9 @@ onBeforeUnmount(() => {
queueStatsConnection.dispose();
});
-const headerActions = $computed(() => []);
+const headerActions = computed(() => []);
-const headerTabs = $computed(() => []);
+const headerTabs = computed(() => []);
definePageMetadata({
title: i18n.ts.dashboard,
diff --git a/packages/frontend/src/pages/admin/proxy-account.vue b/packages/frontend/src/pages/admin/proxy-account.vue
index 3e2a8bdf38..d65e78afa5 100644
--- a/packages/frontend/src/pages/admin/proxy-account.vue
+++ b/packages/frontend/src/pages/admin/proxy-account.vue
@@ -21,7 +21,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { } from 'vue';
+import { ref, computed } from 'vue';
import MkKeyValue from '@/components/MkKeyValue.vue';
import MkButton from '@/components/MkButton.vue';
import MkInfo from '@/components/MkInfo.vue';
@@ -31,36 +31,36 @@ import { fetchInstance } from '@/instance.js';
import { i18n } from '@/i18n.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';
-let proxyAccount: any = $ref(null);
-let proxyAccountId: any = $ref(null);
+const proxyAccount = ref<any>(null);
+const proxyAccountId = ref<any>(null);
async function init() {
const meta = await os.api('admin/meta');
- proxyAccountId = meta.proxyAccountId;
- if (proxyAccountId) {
- proxyAccount = await os.api('users/show', { userId: proxyAccountId });
+ proxyAccountId.value = meta.proxyAccountId;
+ if (proxyAccountId.value) {
+ proxyAccount.value = await os.api('users/show', { userId: proxyAccountId.value });
}
}
function chooseProxyAccount() {
os.selectUser().then(user => {
- proxyAccount = user;
- proxyAccountId = user.id;
+ proxyAccount.value = user;
+ proxyAccountId.value = user.id;
save();
});
}
function save() {
os.apiWithDialog('admin/update-meta', {
- proxyAccountId: proxyAccountId,
+ proxyAccountId: proxyAccountId.value,
}).then(() => {
fetchInstance();
});
}
-const headerActions = $computed(() => []);
+const headerActions = computed(() => []);
-const headerTabs = $computed(() => []);
+const headerTabs = computed(() => []);
definePageMetadata({
title: i18n.ts.proxyAccount,
diff --git a/packages/frontend/src/pages/admin/queue.chart.vue b/packages/frontend/src/pages/admin/queue.chart.vue
index 4746f9de1b..9612f78624 100644
--- a/packages/frontend/src/pages/admin/queue.chart.vue
+++ b/packages/frontend/src/pages/admin/queue.chart.vue
@@ -48,7 +48,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { markRaw, onMounted, onUnmounted, ref } from 'vue';
+import { markRaw, onMounted, onUnmounted, ref, shallowRef } from 'vue';
import XChart from './queue.chart.chart.vue';
import number from '@/filters/number.js';
import * as os from '@/os.js';
@@ -63,10 +63,10 @@ const active = ref(0);
const delayed = ref(0);
const waiting = ref(0);
const jobs = ref([]);
-let chartProcess = $shallowRef<InstanceType<typeof XChart>>();
-let chartActive = $shallowRef<InstanceType<typeof XChart>>();
-let chartDelayed = $shallowRef<InstanceType<typeof XChart>>();
-let chartWaiting = $shallowRef<InstanceType<typeof XChart>>();
+const chartProcess = shallowRef<InstanceType<typeof XChart>>();
+const chartActive = shallowRef<InstanceType<typeof XChart>>();
+const chartDelayed = shallowRef<InstanceType<typeof XChart>>();
+const chartWaiting = shallowRef<InstanceType<typeof XChart>>();
const props = defineProps<{
domain: string;
@@ -78,10 +78,10 @@ const onStats = (stats) => {
delayed.value = stats[props.domain].delayed;
waiting.value = stats[props.domain].waiting;
- chartProcess.pushData(stats[props.domain].activeSincePrevTick);
- chartActive.pushData(stats[props.domain].active);
- chartDelayed.pushData(stats[props.domain].delayed);
- chartWaiting.pushData(stats[props.domain].waiting);
+ chartProcess.value.pushData(stats[props.domain].activeSincePrevTick);
+ chartActive.value.pushData(stats[props.domain].active);
+ chartDelayed.value.pushData(stats[props.domain].delayed);
+ chartWaiting.value.pushData(stats[props.domain].waiting);
};
const onStatsLog = (statsLog) => {
@@ -97,10 +97,10 @@ const onStatsLog = (statsLog) => {
dataWaiting.push(stats[props.domain].waiting);
}
- chartProcess.setData(dataProcess);
- chartActive.setData(dataActive);
- chartDelayed.setData(dataDelayed);
- chartWaiting.setData(dataWaiting);
+ chartProcess.value.setData(dataProcess);
+ chartActive.value.setData(dataActive);
+ chartDelayed.value.setData(dataDelayed);
+ chartWaiting.value.setData(dataWaiting);
};
onMounted(() => {
diff --git a/packages/frontend/src/pages/admin/queue.vue b/packages/frontend/src/pages/admin/queue.vue
index 62a8aa1e22..245c55f6e7 100644
--- a/packages/frontend/src/pages/admin/queue.vue
+++ b/packages/frontend/src/pages/admin/queue.vue
@@ -16,6 +16,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
+import { ref, computed } from 'vue';
import XQueue from './queue.chart.vue';
import XHeader from './_header_.vue';
import * as os from '@/os.js';
@@ -24,7 +25,7 @@ import { i18n } from '@/i18n.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';
import MkButton from '@/components/MkButton.vue';
-let tab = $ref('deliver');
+const tab = ref('deliver');
function clear() {
os.confirm({
@@ -46,20 +47,20 @@ function promoteAllQueues() {
}).then(({ canceled }) => {
if (canceled) return;
- os.apiWithDialog('admin/queue/promote', { type: tab });
+ os.apiWithDialog('admin/queue/promote', { type: tab.value });
});
}
-const headerActions = $computed(() => [{
+const headerActions = computed(() => [{
asFullButton: true,
icon: 'ph-arrow-square-out ph-bold ph-lg',
text: i18n.ts.dashboard,
handler: () => {
- window.open(config.url + '/queue', '_blank');
+ window.open(config.url + '/queue', '_blank', 'noopener');
},
}]);
-const headerTabs = $computed(() => [{
+const headerTabs = computed(() => [{
key: 'deliver',
title: 'Deliver',
}, {
diff --git a/packages/frontend/src/pages/admin/relays.vue b/packages/frontend/src/pages/admin/relays.vue
index 9b4a84d12f..b4d6490e09 100644
--- a/packages/frontend/src/pages/admin/relays.vue
+++ b/packages/frontend/src/pages/admin/relays.vue
@@ -24,14 +24,14 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { } from 'vue';
+import { ref, computed } from 'vue';
import XHeader from './_header_.vue';
import MkButton from '@/components/MkButton.vue';
import * as os from '@/os.js';
import { i18n } from '@/i18n.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';
-let relays: any[] = $ref([]);
+const relays = ref<any[]>([]);
async function addRelay() {
const { canceled, result: inbox } = await os.inputText({
@@ -67,20 +67,20 @@ function remove(inbox: string) {
function refresh() {
os.api('admin/relays/list').then((relayList: any) => {
- relays = relayList;
+ relays.value = relayList;
});
}
refresh();
-const headerActions = $computed(() => [{
+const headerActions = computed(() => [{
asFullButton: true,
icon: 'ph-plus ph-bold ph-lg',
text: i18n.ts.addRelay,
handler: addRelay,
}]);
-const headerTabs = $computed(() => []);
+const headerTabs = computed(() => []);
definePageMetadata({
title: i18n.ts.relays,
diff --git a/packages/frontend/src/pages/admin/roles.edit.vue b/packages/frontend/src/pages/admin/roles.edit.vue
index a982b41e71..b3c06454ae 100644
--- a/packages/frontend/src/pages/admin/roles.edit.vue
+++ b/packages/frontend/src/pages/admin/roles.edit.vue
@@ -22,7 +22,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { computed } from 'vue';
+import { computed, ref } from 'vue';
import { v4 as uuid } from 'uuid';
import XHeader from './_header_.vue';
import XEditor from './roles.editor.vue';
@@ -39,17 +39,17 @@ const props = defineProps<{
id?: string;
}>();
-let role = $ref(null);
-let data = $ref(null);
+const role = ref(null);
+const data = ref(null);
if (props.id) {
- role = await os.api('admin/roles/show', {
+ role.value = await os.api('admin/roles/show', {
roleId: props.id,
});
- data = role;
+ data.value = role.value;
} else {
- data = {
+ data.value = {
name: 'New Role',
description: '',
isAdministrator: false,
@@ -69,24 +69,24 @@ if (props.id) {
async function save() {
rolesCache.delete();
- if (role) {
+ if (role.value) {
os.apiWithDialog('admin/roles/update', {
- roleId: role.id,
- ...data,
+ roleId: role.value.id,
+ ...data.value,
});
- router.push('/admin/roles/' + role.id);
+ router.push('/admin/roles/' + role.value.id);
} else {
const created = await os.apiWithDialog('admin/roles/create', {
- ...data,
+ ...data.value,
});
router.push('/admin/roles/' + created.id);
}
}
-const headerTabs = $computed(() => []);
+const headerTabs = computed(() => []);
-definePageMetadata(computed(() => role ? {
- title: i18n.ts._role.edit + ': ' + role.name,
+definePageMetadata(computed(() => role.value ? {
+ title: i18n.ts._role.edit + ': ' + role.value.name,
icon: 'ph-seal-check ph-bold ph-lg',
} : {
title: i18n.ts._role.new,
diff --git a/packages/frontend/src/pages/admin/roles.editor.vue b/packages/frontend/src/pages/admin/roles.editor.vue
index efdf1ff4f8..164510fd24 100644
--- a/packages/frontend/src/pages/admin/roles.editor.vue
+++ b/packages/frontend/src/pages/admin/roles.editor.vue
@@ -318,7 +318,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</MkRange>
</div>
</MkFolder>
-
+
<MkFolder v-if="matchQuery([i18n.ts._role._options.canSearchNotes, 'canSearchNotes'])">
<template #label>{{ i18n.ts._role._options.canSearchNotes }}</template>
<template #suffix>
@@ -571,13 +571,33 @@ SPDX-License-Identifier: AGPL-3.0-only
</MkRange>
</div>
</MkFolder>
+
+ <MkFolder v-if="matchQuery([i18n.ts._role._options.avatarDecorationLimit, 'avatarDecorationLimit'])">
+ <template #label>{{ i18n.ts._role._options.avatarDecorationLimit }}</template>
+ <template #suffix>
+ <span v-if="role.policies.avatarDecorationLimit.useDefault" :class="$style.useDefaultLabel">{{ i18n.ts._role.useBaseValue }}</span>
+ <span v-else>{{ role.policies.avatarDecorationLimit.value }}</span>
+ <span :class="$style.priorityIndicator"><i :class="getPriorityIcon(role.policies.avatarDecorationLimit)"></i></span>
+ </template>
+ <div class="_gaps">
+ <MkSwitch v-model="role.policies.avatarDecorationLimit.useDefault" :readonly="readonly">
+ <template #label>{{ i18n.ts._role.useBaseValue }}</template>
+ </MkSwitch>
+ <MkInput v-model="role.policies.avatarDecorationLimit.value" type="number" :min="0">
+ <template #label>{{ i18n.ts._role._options.avatarDecorationLimit }}</template>
+ </MkInput>
+ <MkRange v-model="role.policies.avatarDecorationLimit.priority" :min="0" :max="2" :step="1" easing :textConverter="(v) => v === 0 ? i18n.ts._role._priority.low : v === 1 ? i18n.ts._role._priority.middle : v === 2 ? i18n.ts._role._priority.high : ''">
+ <template #label>{{ i18n.ts._role.priority }}</template>
+ </MkRange>
+ </div>
+ </MkFolder>
</div>
</FormSlot>
</div>
</template>
<script lang="ts" setup>
-import { watch } from 'vue';
+import { watch, ref, computed } from 'vue';
import { throttle } from 'throttle-debounce';
import RolesEditorFormula from './RolesEditorFormula.vue';
import MkInput from '@/components/MkInput.vue';
@@ -589,7 +609,7 @@ import MkSwitch from '@/components/MkSwitch.vue';
import MkRange from '@/components/MkRange.vue';
import FormSlot from '@/components/form/slot.vue';
import { i18n } from '@/i18n.js';
-import { ROLE_POLICIES } from '@/const';
+import { ROLE_POLICIES } from '@/const.js';
import { instance } from '@/instance.js';
import { deepClone } from '@/scripts/clone.js';
@@ -602,12 +622,12 @@ const props = defineProps<{
readonly?: boolean;
}>();
-let role = $ref(deepClone(props.modelValue));
+const role = ref(deepClone(props.modelValue));
// fill missing policy
for (const ROLE_POLICY of ROLE_POLICIES) {
- if (role.policies[ROLE_POLICY] == null) {
- role.policies[ROLE_POLICY] = {
+ if (role.value.policies[ROLE_POLICY] == null) {
+ role.value.policies[ROLE_POLICY] = {
useDefault: true,
priority: 0,
value: instance.policies[ROLE_POLICY],
@@ -615,15 +635,15 @@ for (const ROLE_POLICY of ROLE_POLICIES) {
}
}
-let rolePermission = $computed({
- get: () => role.isAdministrator ? 'administrator' : role.isModerator ? 'moderator' : 'normal',
+const rolePermission = computed({
+ get: () => role.value.isAdministrator ? 'administrator' : role.value.isModerator ? 'moderator' : 'normal',
set: (val) => {
- role.isAdministrator = val === 'administrator';
- role.isModerator = val === 'moderator';
+ role.value.isAdministrator = val === 'administrator';
+ role.value.isModerator = val === 'moderator';
},
});
-let q = $ref('');
+const q = ref('');
function getPriorityIcon(option) {
if (option.priority === 2) return 'ph-arrow-up ph-bold ph-lg';
@@ -632,32 +652,32 @@ function getPriorityIcon(option) {
}
function matchQuery(keywords: string[]): boolean {
- if (q.trim().length === 0) return true;
- return keywords.some(keyword => keyword.toLowerCase().includes(q.toLowerCase()));
+ if (q.value.trim().length === 0) return true;
+ return keywords.some(keyword => keyword.toLowerCase().includes(q.value.toLowerCase()));
}
const save = throttle(100, () => {
const data = {
- name: role.name,
- description: role.description,
- color: role.color === '' ? null : role.color,
- iconUrl: role.iconUrl === '' ? null : role.iconUrl,
- displayOrder: role.displayOrder,
- target: role.target,
- condFormula: role.condFormula,
- isAdministrator: role.isAdministrator,
- isModerator: role.isModerator,
- isPublic: role.isPublic,
- isExplorable: role.isExplorable,
- asBadge: role.asBadge,
- canEditMembersByModerator: role.canEditMembersByModerator,
- policies: role.policies,
+ name: role.value.name,
+ description: role.value.description,
+ color: role.value.color === '' ? null : role.value.color,
+ iconUrl: role.value.iconUrl === '' ? null : role.value.iconUrl,
+ displayOrder: role.value.displayOrder,
+ target: role.value.target,
+ condFormula: role.value.condFormula,
+ isAdministrator: role.value.isAdministrator,
+ isModerator: role.value.isModerator,
+ isPublic: role.value.isPublic,
+ isExplorable: role.value.isExplorable,
+ asBadge: role.value.asBadge,
+ canEditMembersByModerator: role.value.canEditMembersByModerator,
+ policies: role.value.policies,
};
emit('update:modelValue', data);
});
-watch($$(role), save, { deep: true });
+watch(role, save, { deep: true });
</script>
<style lang="scss" module>
diff --git a/packages/frontend/src/pages/admin/roles.role.vue b/packages/frontend/src/pages/admin/roles.role.vue
index 953db11a1a..92818cc3de 100644
--- a/packages/frontend/src/pages/admin/roles.role.vue
+++ b/packages/frontend/src/pages/admin/roles.role.vue
@@ -62,7 +62,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { computed, reactive } from 'vue';
+import { computed, reactive, ref } from 'vue';
import XHeader from './_header_.vue';
import XEditor from './roles.editor.vue';
import MkFolder from '@/components/MkFolder.vue';
@@ -73,7 +73,7 @@ import { useRouter } from '@/router.js';
import MkButton from '@/components/MkButton.vue';
import MkUserCardMini from '@/components/MkUserCardMini.vue';
import MkInfo from '@/components/MkInfo.vue';
-import MkPagination, { Paging } from '@/components/MkPagination.vue';
+import MkPagination from '@/components/MkPagination.vue';
import { infoImageUrl } from '@/instance.js';
const router = useRouter();
@@ -90,7 +90,7 @@ const usersPagination = {
})),
};
-let expandedItems = $ref([]);
+const expandedItems = ref([]);
const role = reactive(await os.api('admin/roles/show', {
roleId: props.id,
@@ -160,16 +160,16 @@ async function unassign(user, ev) {
}
async function toggleItem(item) {
- if (expandedItems.includes(item.id)) {
- expandedItems = expandedItems.filter(x => x !== item.id);
+ if (expandedItems.value.includes(item.id)) {
+ expandedItems.value = expandedItems.value.filter(x => x !== item.id);
} else {
- expandedItems.push(item.id);
+ expandedItems.value.push(item.id);
}
}
-const headerActions = $computed(() => []);
+const headerActions = computed(() => []);
-const headerTabs = $computed(() => []);
+const headerTabs = computed(() => []);
definePageMetadata(computed(() => ({
title: i18n.ts.role + ': ' + role.name,
diff --git a/packages/frontend/src/pages/admin/roles.vue b/packages/frontend/src/pages/admin/roles.vue
index 7fedb87d41..07cce6d9a5 100644
--- a/packages/frontend/src/pages/admin/roles.vue
+++ b/packages/frontend/src/pages/admin/roles.vue
@@ -211,19 +211,26 @@ SPDX-License-Identifier: AGPL-3.0-only
</MkSwitch>
</MkFolder>
+ <MkFolder v-if="matchQuery([i18n.ts._role._options.avatarDecorationLimit, 'avatarDecorationLimit'])">
+ <template #label>{{ i18n.ts._role._options.avatarDecorationLimit }}</template>
+ <template #suffix>{{ policies.avatarDecorationLimit }}</template>
+ <MkInput v-model="policies.avatarDecorationLimit" type="number" :min="0">
+ </MkInput>
+ </MkFolder>
+
<MkButton primary rounded @click="updateBaseRole">{{ i18n.ts.save }}</MkButton>
</div>
</MkFolder>
<MkButton primary rounded @click="create"><i class="ph-plus ph-bold ph-lg"></i> {{ i18n.ts._role.new }}</MkButton>
<div class="_gaps_s">
<MkFoldableSection>
- <template #header>Manual roles</template>
+ <template #header>{{ i18n.ts._role.manualRoles }}</template>
<div class="_gaps_s">
<MkRolePreview v-for="role in roles.filter(x => x.target === 'manual')" :key="role.id" :role="role" :forModeration="true"/>
</div>
</MkFoldableSection>
<MkFoldableSection>
- <template #header>Conditional roles</template>
+ <template #header>{{ i18n.ts._role.conditionalRoles }}</template>
<div class="_gaps_s">
<MkRolePreview v-for="role in roles.filter(x => x.target === 'conditional')" :key="role.id" :role="role" :forModeration="true"/>
</div>
@@ -278,9 +285,9 @@ function create() {
router.push('/admin/roles/new');
}
-const headerActions = $computed(() => []);
+const headerActions = computed(() => []);
-const headerTabs = $computed(() => []);
+const headerTabs = computed(() => []);
definePageMetadata(computed(() => ({
title: i18n.ts.roles,
diff --git a/packages/frontend/src/pages/admin/security.vue b/packages/frontend/src/pages/admin/security.vue
index f02fa1024d..95f4d2b20c 100644
--- a/packages/frontend/src/pages/admin/security.vue
+++ b/packages/frontend/src/pages/admin/security.vue
@@ -30,6 +30,13 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkSwitch v-model="enableActiveEmailValidation" @update:modelValue="save">
<template #label>Enable</template>
</MkSwitch>
+ <MkSwitch v-model="enableVerifymailApi" @update:modelValue="save">
+ <template #label>Use Verifymail API</template>
+ </MkSwitch>
+ <MkInput v-model="verifymailAuthKey" @update:modelValue="save">
+ <template #prefix><i class="ti ti-key"></i></template>
+ <template #label>Verifymail API Auth Key</template>
+ </MkInput>
</div>
</MkFolder>
@@ -64,7 +71,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { } from 'vue';
+import { ref, computed } from 'vue';
import XBotProtection from './bot-protection.vue';
import XHeader from './_header_.vue';
import MkFolder from '@/components/MkFolder.vue';
@@ -79,36 +86,42 @@ import { fetchInstance } from '@/instance.js';
import { i18n } from '@/i18n.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';
-let summalyProxy: string = $ref('');
-let enableHcaptcha: boolean = $ref(false);
-let enableRecaptcha: boolean = $ref(false);
-let enableTurnstile: boolean = $ref(false);
-let enableIpLogging: boolean = $ref(false);
-let enableActiveEmailValidation: boolean = $ref(false);
+const summalyProxy = ref<string>('');
+const enableHcaptcha = ref<boolean>(false);
+const enableRecaptcha = ref<boolean>(false);
+const enableTurnstile = ref<boolean>(false);
+const enableIpLogging = ref<boolean>(false);
+const enableActiveEmailValidation = ref<boolean>(false);
+const enableVerifymailApi = ref<boolean>(false);
+const verifymailAuthKey = ref<string | null>(null);
async function init() {
const meta = await os.api('admin/meta');
- summalyProxy = meta.summalyProxy;
- enableHcaptcha = meta.enableHcaptcha;
- enableRecaptcha = meta.enableRecaptcha;
- enableTurnstile = meta.enableTurnstile;
- enableIpLogging = meta.enableIpLogging;
- enableActiveEmailValidation = meta.enableActiveEmailValidation;
+ summalyProxy.value = meta.summalyProxy;
+ enableHcaptcha.value = meta.enableHcaptcha;
+ enableRecaptcha.value = meta.enableRecaptcha;
+ enableTurnstile.value = meta.enableTurnstile;
+ enableIpLogging.value = meta.enableIpLogging;
+ enableActiveEmailValidation.value = meta.enableActiveEmailValidation;
+ enableVerifymailApi.value = meta.enableVerifymailApi;
+ verifymailAuthKey.value = meta.verifymailAuthKey;
}
function save() {
os.apiWithDialog('admin/update-meta', {
- summalyProxy,
- enableIpLogging,
- enableActiveEmailValidation,
+ summalyProxy: summalyProxy.value,
+ enableIpLogging: enableIpLogging.value,
+ enableActiveEmailValidation: enableActiveEmailValidation.value,
+ enableVerifymailApi: enableVerifymailApi.value,
+ verifymailAuthKey: verifymailAuthKey.value,
}).then(() => {
fetchInstance();
});
}
-const headerActions = $computed(() => []);
+const headerActions = computed(() => []);
-const headerTabs = $computed(() => []);
+const headerTabs = computed(() => []);
definePageMetadata({
title: i18n.ts.security,
diff --git a/packages/frontend/src/pages/admin/server-rules.vue b/packages/frontend/src/pages/admin/server-rules.vue
index 4302bde91d..6aecb43399 100644
--- a/packages/frontend/src/pages/admin/server-rules.vue
+++ b/packages/frontend/src/pages/admin/server-rules.vue
@@ -41,7 +41,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { defineAsyncComponent } from 'vue';
+import { defineAsyncComponent, ref, computed } from 'vue';
import XHeader from './_header_.vue';
import * as os from '@/os.js';
import { fetchInstance, instance } from '@/instance.js';
@@ -52,20 +52,20 @@ import MkInput from '@/components/MkInput.vue';
const Sortable = defineAsyncComponent(() => import('vuedraggable').then(x => x.default));
-let serverRules: string[] = $ref(instance.serverRules);
+const serverRules = ref<string[]>(instance.serverRules);
const save = async () => {
await os.apiWithDialog('admin/update-meta', {
- serverRules,
+ serverRules: serverRules.value,
});
fetchInstance();
};
const remove = (index: number): void => {
- serverRules.splice(index, 1);
+ serverRules.value.splice(index, 1);
};
-const headerTabs = $computed(() => []);
+const headerTabs = computed(() => []);
definePageMetadata({
title: i18n.ts.serverRules,
diff --git a/packages/frontend/src/pages/admin/settings.vue b/packages/frontend/src/pages/admin/settings.vue
index db117d5061..07d7acf11c 100644
--- a/packages/frontend/src/pages/admin/settings.vue
+++ b/packages/frontend/src/pages/admin/settings.vue
@@ -148,7 +148,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { } from 'vue';
+import { ref, computed } from 'vue';
import XHeader from './_header_.vue';
import MkSwitch from '@/components/MkSwitch.vue';
import MkInput from '@/components/MkInput.vue';
@@ -163,76 +163,76 @@ import { i18n } from '@/i18n.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';
import MkButton from '@/components/MkButton.vue';
-let name: string | null = $ref(null);
-let shortName: string | null = $ref(null);
-let description: string | null = $ref(null);
-let maintainerName: string | null = $ref(null);
-let maintainerEmail: string | null = $ref(null);
-let impressumUrl: string | null = $ref(null);
-let pinnedUsers: string = $ref('');
-let cacheRemoteFiles: boolean = $ref(false);
-let cacheRemoteSensitiveFiles: boolean = $ref(false);
-let enableServiceWorker: boolean = $ref(false);
-let swPublicKey: any = $ref(null);
-let swPrivateKey: any = $ref(null);
-let enableFanoutTimeline: boolean = $ref(false);
-let enableFanoutTimelineDbFallback: boolean = $ref(false);
-let perLocalUserUserTimelineCacheMax: number = $ref(0);
-let perRemoteUserUserTimelineCacheMax: number = $ref(0);
-let perUserHomeTimelineCacheMax: number = $ref(0);
-let perUserListTimelineCacheMax: number = $ref(0);
-let notesPerOneAd: number = $ref(0);
+const name = ref<string | null>(null);
+const shortName = ref<string | null>(null);
+const description = ref<string | null>(null);
+const maintainerName = ref<string | null>(null);
+const maintainerEmail = ref<string | null>(null);
+const impressumUrl = ref<string | null>(null);
+const pinnedUsers = ref<string>('');
+const cacheRemoteFiles = ref<boolean>(false);
+const cacheRemoteSensitiveFiles = ref<boolean>(false);
+const enableServiceWorker = ref<boolean>(false);
+const swPublicKey = ref<any>(null);
+const swPrivateKey = ref<any>(null);
+const enableFanoutTimeline = ref<boolean>(false);
+const enableFanoutTimelineDbFallback = ref<boolean>(false);
+const perLocalUserUserTimelineCacheMax = ref<number>(0);
+const perRemoteUserUserTimelineCacheMax = ref<number>(0);
+const perUserHomeTimelineCacheMax = ref<number>(0);
+const perUserListTimelineCacheMax = ref<number>(0);
+const notesPerOneAd = ref<number>(0);
async function init(): Promise<void> {
const meta = await os.api('admin/meta');
- name = meta.name;
- shortName = meta.shortName;
- description = meta.description;
- maintainerName = meta.maintainerName;
- maintainerEmail = meta.maintainerEmail;
- impressumUrl = meta.impressumUrl;
- pinnedUsers = meta.pinnedUsers.join('\n');
- cacheRemoteFiles = meta.cacheRemoteFiles;
- cacheRemoteSensitiveFiles = meta.cacheRemoteSensitiveFiles;
- enableServiceWorker = meta.enableServiceWorker;
- swPublicKey = meta.swPublickey;
- swPrivateKey = meta.swPrivateKey;
- enableFanoutTimeline = meta.enableFanoutTimeline;
- enableFanoutTimelineDbFallback = meta.enableFanoutTimelineDbFallback;
- perLocalUserUserTimelineCacheMax = meta.perLocalUserUserTimelineCacheMax;
- perRemoteUserUserTimelineCacheMax = meta.perRemoteUserUserTimelineCacheMax;
- perUserHomeTimelineCacheMax = meta.perUserHomeTimelineCacheMax;
- perUserListTimelineCacheMax = meta.perUserListTimelineCacheMax;
- notesPerOneAd = meta.notesPerOneAd;
+ name.value = meta.name;
+ shortName.value = meta.shortName;
+ description.value = meta.description;
+ maintainerName.value = meta.maintainerName;
+ maintainerEmail.value = meta.maintainerEmail;
+ impressumUrl.value = meta.impressumUrl;
+ pinnedUsers.value = meta.pinnedUsers.join('\n');
+ cacheRemoteFiles.value = meta.cacheRemoteFiles;
+ cacheRemoteSensitiveFiles.value = meta.cacheRemoteSensitiveFiles;
+ enableServiceWorker.value = meta.enableServiceWorker;
+ swPublicKey.value = meta.swPublickey;
+ swPrivateKey.value = meta.swPrivateKey;
+ enableFanoutTimeline.value = meta.enableFanoutTimeline;
+ enableFanoutTimelineDbFallback.value = meta.enableFanoutTimelineDbFallback;
+ perLocalUserUserTimelineCacheMax.value = meta.perLocalUserUserTimelineCacheMax;
+ perRemoteUserUserTimelineCacheMax.value = meta.perRemoteUserUserTimelineCacheMax;
+ perUserHomeTimelineCacheMax.value = meta.perUserHomeTimelineCacheMax;
+ perUserListTimelineCacheMax.value = meta.perUserListTimelineCacheMax;
+ notesPerOneAd.value = meta.notesPerOneAd;
}
async function save(): void {
await os.apiWithDialog('admin/update-meta', {
- name,
- shortName: shortName === '' ? null : shortName,
- description,
- maintainerName,
- maintainerEmail,
- impressumUrl,
- pinnedUsers: pinnedUsers.split('\n'),
- cacheRemoteFiles,
- cacheRemoteSensitiveFiles,
- enableServiceWorker,
- swPublicKey,
- swPrivateKey,
- enableFanoutTimeline,
- enableFanoutTimelineDbFallback,
- perLocalUserUserTimelineCacheMax,
- perRemoteUserUserTimelineCacheMax,
- perUserHomeTimelineCacheMax,
- perUserListTimelineCacheMax,
- notesPerOneAd,
+ name: name.value,
+ shortName: shortName.value === '' ? null : shortName.value,
+ description: description.value,
+ maintainerName: maintainerName.value,
+ maintainerEmail: maintainerEmail.value,
+ impressumUrl: impressumUrl.value,
+ pinnedUsers: pinnedUsers.value.split('\n'),
+ cacheRemoteFiles: cacheRemoteFiles.value,
+ cacheRemoteSensitiveFiles: cacheRemoteSensitiveFiles.value,
+ enableServiceWorker: enableServiceWorker.value,
+ swPublicKey: swPublicKey.value,
+ swPrivateKey: swPrivateKey.value,
+ enableFanoutTimeline: enableFanoutTimeline.value,
+ enableFanoutTimelineDbFallback: enableFanoutTimelineDbFallback.value,
+ perLocalUserUserTimelineCacheMax: perLocalUserUserTimelineCacheMax.value,
+ perRemoteUserUserTimelineCacheMax: perRemoteUserUserTimelineCacheMax.value,
+ perUserHomeTimelineCacheMax: perUserHomeTimelineCacheMax.value,
+ perUserListTimelineCacheMax: perUserListTimelineCacheMax.value,
+ notesPerOneAd: notesPerOneAd.value,
});
fetchInstance();
}
-const headerTabs = $computed(() => []);
+const headerTabs = computed(() => []);
definePageMetadata({
title: i18n.ts.general,
diff --git a/packages/frontend/src/pages/admin/users.vue b/packages/frontend/src/pages/admin/users.vue
index 7f809ed9cc..1bc4eb4089 100644
--- a/packages/frontend/src/pages/admin/users.vue
+++ b/packages/frontend/src/pages/admin/users.vue
@@ -58,7 +58,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { computed } from 'vue';
+import { computed, shallowRef, ref } from 'vue';
import XHeader from './_header_.vue';
import MkInput from '@/components/MkInput.vue';
import MkSelect from '@/components/MkSelect.vue';
@@ -70,22 +70,22 @@ import { definePageMetadata } from '@/scripts/page-metadata.js';
import MkUserCardMini from '@/components/MkUserCardMini.vue';
import { dateString } from '@/filters/date.js';
-let paginationComponent = $shallowRef<InstanceType<typeof MkPagination>>();
+const paginationComponent = shallowRef<InstanceType<typeof MkPagination>>();
-let sort = $ref('+createdAt');
-let state = $ref('all');
-let origin = $ref('local');
-let searchUsername = $ref('');
-let searchHost = $ref('');
+const sort = ref('+createdAt');
+const state = ref('all');
+const origin = ref('local');
+const searchUsername = ref('');
+const searchHost = ref('');
const pagination = {
endpoint: 'admin/show-users' as const,
limit: 10,
params: computed(() => ({
- sort: sort,
- state: state,
- origin: origin,
- username: searchUsername,
- hostname: searchHost,
+ sort: sort.value,
+ state: state.value,
+ origin: origin.value,
+ username: searchUsername.value,
+ hostname: searchHost.value,
})),
offsetMode: true,
};
@@ -112,7 +112,7 @@ async function addUser() {
username: username,
password: password,
}).then(res => {
- paginationComponent.reload();
+ paginationComponent.value.reload();
});
}
@@ -120,7 +120,7 @@ function show(user) {
os.pageWindow(`/admin/user/${user.id}`);
}
-const headerActions = $computed(() => [{
+const headerActions = computed(() => [{
icon: 'ph-magnifying-glass ph-bold ph-lg',
text: i18n.ts.search,
handler: searchUser,
@@ -136,7 +136,7 @@ const headerActions = $computed(() => [{
handler: lookupUser,
}]);
-const headerTabs = $computed(() => []);
+const headerTabs = computed(() => []);
definePageMetadata(computed(() => ({
title: i18n.ts.users,
diff --git a/packages/frontend/src/pages/ads.vue b/packages/frontend/src/pages/ads.vue
index bee3af39ef..9d508937af 100644
--- a/packages/frontend/src/pages/ads.vue
+++ b/packages/frontend/src/pages/ads.vue
@@ -16,8 +16,6 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { computed, watch } from 'vue';
-import * as os from '@/os.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';
import { i18n } from '@/i18n.js';
import { instance } from '@/instance.js';
diff --git a/packages/frontend/src/pages/announcements.vue b/packages/frontend/src/pages/announcements.vue
index 51e1efda53..705115abb0 100644
--- a/packages/frontend/src/pages/announcements.vue
+++ b/packages/frontend/src/pages/announcements.vue
@@ -40,7 +40,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { ref } from 'vue';
+import { ref, computed } from 'vue';
import MkPagination from '@/components/MkPagination.vue';
import MkButton from '@/components/MkButton.vue';
import MkInfo from '@/components/MkInfo.vue';
@@ -90,9 +90,9 @@ async function read(announcement) {
});
}
-const headerActions = $computed(() => []);
+const headerActions = computed(() => []);
-const headerTabs = $computed(() => [{
+const headerTabs = computed(() => [{
key: 'current',
title: i18n.ts.currentAnnouncements,
icon: 'ph-fire ph-bold ph-lg',
diff --git a/packages/frontend/src/pages/antenna-timeline.vue b/packages/frontend/src/pages/antenna-timeline.vue
index dfba117799..c8a4c3f8dc 100644
--- a/packages/frontend/src/pages/antenna-timeline.vue
+++ b/packages/frontend/src/pages/antenna-timeline.vue
@@ -24,7 +24,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { computed, watch } from 'vue';
+import { computed, watch, ref, shallowRef } from 'vue';
import MkTimeline from '@/components/MkTimeline.vue';
import { scroll } from '@/scripts/scroll.js';
import * as os from '@/os.js';
@@ -38,20 +38,20 @@ const props = defineProps<{
antennaId: string;
}>();
-let antenna = $ref(null);
-let queue = $ref(0);
-let rootEl = $shallowRef<HTMLElement>();
-let tlEl = $shallowRef<InstanceType<typeof MkTimeline>>();
-const keymap = $computed(() => ({
+const antenna = ref(null);
+const queue = ref(0);
+const rootEl = shallowRef<HTMLElement>();
+const tlEl = shallowRef<InstanceType<typeof MkTimeline>>();
+const keymap = computed(() => ({
't': focus,
}));
function queueUpdated(q) {
- queue = q;
+ queue.value = q;
}
function top() {
- scroll(rootEl, { top: 0 });
+ scroll(rootEl.value, { top: 0 });
}
async function timetravel() {
@@ -60,7 +60,7 @@ async function timetravel() {
});
if (canceled) return;
- tlEl.timetravel(date);
+ tlEl.value.timetravel(date);
}
function settings() {
@@ -68,16 +68,16 @@ function settings() {
}
function focus() {
- tlEl.focus();
+ tlEl.value.focus();
}
watch(() => props.antennaId, async () => {
- antenna = await os.api('antennas/show', {
+ antenna.value = await os.api('antennas/show', {
antennaId: props.antennaId,
});
}, { immediate: true });
-const headerActions = $computed(() => antenna ? [{
+const headerActions = computed(() => antenna.value ? [{
icon: 'ph-calendar ph-bold ph-lg',
text: i18n.ts.jumpToSpecifiedDate,
handler: timetravel,
@@ -87,10 +87,10 @@ const headerActions = $computed(() => antenna ? [{
handler: settings,
}] : []);
-const headerTabs = $computed(() => []);
+const headerTabs = computed(() => []);
-definePageMetadata(computed(() => antenna ? {
- title: antenna.name,
+definePageMetadata(computed(() => antenna.value ? {
+ title: antenna.value.name,
icon: 'ph-flying-saucer ph-bold ph-lg',
} : null));
</script>
diff --git a/packages/frontend/src/pages/api-console.vue b/packages/frontend/src/pages/api-console.vue
index 93ecbdf0cc..946ff3b7ba 100644
--- a/packages/frontend/src/pages/api-console.vue
+++ b/packages/frontend/src/pages/api-console.vue
@@ -34,7 +34,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { ref } from 'vue';
+import { ref, computed } from 'vue';
import JSON5 from 'json5';
import { Endpoints } from 'misskey-js';
import MkButton from '@/components/MkButton.vue';
@@ -83,9 +83,9 @@ function onEndpointChange() {
});
}
-const headerActions = $computed(() => []);
+const headerActions = computed(() => []);
-const headerTabs = $computed(() => []);
+const headerTabs = computed(() => []);
definePageMetadata({
title: 'API console',
diff --git a/packages/frontend/src/pages/auth.form.vue b/packages/frontend/src/pages/auth.form.vue
index 3f6f58df55..8a17e5895d 100644
--- a/packages/frontend/src/pages/auth.form.vue
+++ b/packages/frontend/src/pages/auth.form.vue
@@ -20,14 +20,14 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { } from 'vue';
+import { computed } from 'vue';
import * as Misskey from 'misskey-js';
import MkButton from '@/components/MkButton.vue';
import * as os from '@/os.js';
import { i18n } from '@/i18n.js';
const props = defineProps<{
- session: Misskey.entities.AuthSession;
+ session: Misskey.entities.AuthSessionShowResponse;
}>();
const emit = defineEmits<{
@@ -35,11 +35,11 @@ const emit = defineEmits<{
(event: 'denied'): void;
}>();
-const app = $computed(() => props.session.app);
+const app = computed(() => props.session.app);
-const name = $computed(() => {
+const name = computed(() => {
const el = document.createElement('div');
- el.textContent = app.name;
+ el.textContent = app.value.name;
return el.innerHTML;
});
diff --git a/packages/frontend/src/pages/auth.vue b/packages/frontend/src/pages/auth.vue
index d6c12e7883..d97e89842d 100644
--- a/packages/frontend/src/pages/auth.vue
+++ b/packages/frontend/src/pages/auth.vue
@@ -42,7 +42,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { onMounted } from 'vue';
+import { onMounted, ref, computed } from 'vue';
import * as Misskey from 'misskey-js';
import XForm from './auth.form.vue';
import MkSignin from '@/components/MkSignin.vue';
@@ -58,33 +58,33 @@ const props = defineProps<{
const getUrlParams = () =>
window.location.search
.substring(1)
- .split("&")
+ .split('&')
.reduce((result, query) => {
- const [k, v] = query.split("=");
+ const [k, v] = query.split('=');
result[k] = decodeURI(v);
return result;
}, {});
-let state = $ref<'waiting' | 'accepted' | 'fetch-session-error' | 'denied' | null>(null);
-let session = $ref<Misskey.entities.AuthSession | null>(null);
+const state = ref<'waiting' | 'accepted' | 'fetch-session-error' | 'denied' | null>(null);
+const session = ref<Misskey.entities.AuthSessionShowResponse | null>(null);
function accepted() {
- state = 'accepted';
+ state.value = 'accepted';
const isMastodon = !!getUrlParams().mastodon;
- if (session && session.app.callbackUrl && isMastodon) {
+ if (session.value && session.value.app.callbackUrl && isMastodon) {
const redirectUri = decodeURIComponent(getUrlParams().redirect_uri);
- if (!session.app.callbackUrl.includes('elk.zone') && !session.app.callbackUrl.split("\n").includes(redirectUri)) {
- state = "fetch-session-error";
- throw new Error("Callback URI doesn't match registered app");
+ if (!session.value.app.callbackUrl.includes('elk.zone') && !session.value.app.callbackUrl.split('\n').includes(redirectUri)) {
+ state.value = 'fetch-session-error';
+ throw new Error('Callback URI doesn\'t match registered app');
}
- const callbackUrl = session.app.callbackUrl.includes('elk.zone') ? new URL(session.app.callbackUrl) : new URL(redirectUri);
- callbackUrl.searchParams.append("code", session.token);
- if (getUrlParams().state) callbackUrl.searchParams.append("state", getUrlParams().state);
+ const callbackUrl = session.value.app.callbackUrl.includes('elk.zone') ? new URL(session.value.app.callbackUrl) : new URL(redirectUri);
+ callbackUrl.searchParams.append('code', session.value.token);
+ if (getUrlParams().state) callbackUrl.searchParams.append('state', getUrlParams().state);
location.href = callbackUrl.toString();
- } else if (session && session.app.callbackUrl) {
- const url = new URL(session.app.callbackUrl);
+ } else if (session.value && session.value.app.callbackUrl) {
+ const url = new URL(session.value.app.callbackUrl);
if (['javascript:', 'file:', 'data:', 'mailto:', 'tel:'].includes(url.protocol)) throw new Error('invalid url');
- location.href = `${session.app.callbackUrl}?token=${session.token}`;
+ location.href = `${session.value.app.callbackUrl}?token=${session.value.token}`;
}
}
@@ -96,27 +96,27 @@ onMounted(async () => {
if (!$i) return;
try {
- session = await os.api('auth/session/show', {
+ session.value = await os.api('auth/session/show', {
token: props.token,
});
// 既に連携していた場合
- if (session.app.isAuthorized) {
+ if (session.value.app.isAuthorized) {
await os.api('auth/accept', {
- token: session.token,
+ token: session.value.token,
});
accepted();
} else {
- state = 'waiting';
+ state.value = 'waiting';
}
} catch (err) {
- state = 'fetch-session-error';
+ state.value = 'fetch-session-error';
}
});
-const headerActions = $computed(() => []);
+const headerActions = computed(() => []);
-const headerTabs = $computed(() => []);
+const headerTabs = computed(() => []);
definePageMetadata({
title: i18n.ts._auth.shareAccessTitle,
diff --git a/packages/frontend/src/pages/avatar-decorations.vue b/packages/frontend/src/pages/avatar-decorations.vue
index 47faeb5940..8e2eb2df25 100644
--- a/packages/frontend/src/pages/avatar-decorations.vue
+++ b/packages/frontend/src/pages/avatar-decorations.vue
@@ -34,22 +34,19 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { } from 'vue';
+import { ref, computed } from 'vue';
import MkButton from '@/components/MkButton.vue';
import MkInput from '@/components/MkInput.vue';
import MkTextarea from '@/components/MkTextarea.vue';
-import MkSwitch from '@/components/MkSwitch.vue';
-import MkRadios from '@/components/MkRadios.vue';
-import MkInfo from '@/components/MkInfo.vue';
import * as os from '@/os.js';
import { i18n } from '@/i18n.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';
import MkFolder from '@/components/MkFolder.vue';
-let avatarDecorations: any[] = $ref([]);
+const avatarDecorations = ref<any[]>([]);
function add() {
- avatarDecorations.unshift({
+ avatarDecorations.value.unshift({
_id: Math.random().toString(36),
id: null,
name: '',
@@ -64,7 +61,7 @@ function del(avatarDecoration) {
text: i18n.t('deleteAreYouSure', { x: avatarDecoration.name }),
}).then(({ canceled }) => {
if (canceled) return;
- avatarDecorations = avatarDecorations.filter(x => x !== avatarDecoration);
+ avatarDecorations.value = avatarDecorations.value.filter(x => x !== avatarDecoration);
os.api('admin/avatar-decorations/delete', avatarDecoration);
});
}
@@ -80,20 +77,20 @@ async function save(avatarDecoration) {
function load() {
os.api('admin/avatar-decorations/list').then(_avatarDecorations => {
- avatarDecorations = _avatarDecorations;
+ avatarDecorations.value = _avatarDecorations;
});
}
load();
-const headerActions = $computed(() => [{
+const headerActions = computed(() => [{
asFullButton: true,
icon: 'ph-plus ph-bold ph-lg',
text: i18n.ts.add,
handler: add,
}]);
-const headerTabs = $computed(() => []);
+const headerTabs = computed(() => []);
definePageMetadata({
title: i18n.ts.avatarDecorations,
diff --git a/packages/frontend/src/pages/channel-editor.vue b/packages/frontend/src/pages/channel-editor.vue
index acff4211d1..5408536bb9 100644
--- a/packages/frontend/src/pages/channel-editor.vue
+++ b/packages/frontend/src/pages/channel-editor.vue
@@ -12,7 +12,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template #label>{{ i18n.ts.name }}</template>
</MkInput>
- <MkTextarea v-model="description">
+ <MkTextarea v-model="description" mfmAutocomplete :mfmPreview="true">
<template #label>{{ i18n.ts.description }}</template>
</MkTextarea>
@@ -70,7 +70,6 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { computed, ref, watch, defineAsyncComponent } from 'vue';
-import MkTextarea from '@/components/MkTextarea.vue';
import MkButton from '@/components/MkButton.vue';
import MkInput from '@/components/MkInput.vue';
import MkColorInput from '@/components/MkColorInput.vue';
@@ -81,6 +80,7 @@ import { definePageMetadata } from '@/scripts/page-metadata.js';
import { i18n } from '@/i18n.js';
import MkFolder from '@/components/MkFolder.vue';
import MkSwitch from '@/components/MkSwitch.vue';
+import MkTextarea from '@/components/MkTextarea.vue';
const Sortable = defineAsyncComponent(() => import('vuedraggable').then(x => x.default));
@@ -90,22 +90,22 @@ const props = defineProps<{
channelId?: string;
}>();
-let channel = $ref(null);
-let name = $ref(null);
-let description = $ref(null);
-let bannerUrl = $ref<string | null>(null);
-let bannerId = $ref<string | null>(null);
-let color = $ref('#000');
-let isSensitive = $ref(false);
-let allowRenoteToExternal = $ref(true);
+const channel = ref(null);
+const name = ref(null);
+const description = ref(null);
+const bannerUrl = ref<string | null>(null);
+const bannerId = ref<string | null>(null);
+const color = ref('#000');
+const isSensitive = ref(false);
+const allowRenoteToExternal = ref(true);
const pinnedNotes = ref([]);
-watch(() => bannerId, async () => {
- if (bannerId == null) {
- bannerUrl = null;
+watch(() => bannerId.value, async () => {
+ if (bannerId.value == null) {
+ bannerUrl.value = null;
} else {
- bannerUrl = (await os.api('drive/files/show', {
- fileId: bannerId,
+ bannerUrl.value = (await os.api('drive/files/show', {
+ fileId: bannerId.value,
})).url;
}
});
@@ -113,20 +113,20 @@ watch(() => bannerId, async () => {
async function fetchChannel() {
if (props.channelId == null) return;
- channel = await os.api('channels/show', {
+ channel.value = await os.api('channels/show', {
channelId: props.channelId,
});
- name = channel.name;
- description = channel.description;
- bannerId = channel.bannerId;
- bannerUrl = channel.bannerUrl;
- isSensitive = channel.isSensitive;
- pinnedNotes.value = channel.pinnedNoteIds.map(id => ({
+ name.value = channel.value.name;
+ description.value = channel.value.description;
+ bannerId.value = channel.value.bannerId;
+ bannerUrl.value = channel.value.bannerUrl;
+ isSensitive.value = channel.value.isSensitive;
+ pinnedNotes.value = channel.value.pinnedNoteIds.map(id => ({
id,
}));
- color = channel.color;
- allowRenoteToExternal = channel.allowRenoteToExternal;
+ color.value = channel.value.color;
+ allowRenoteToExternal.value = channel.value.allowRenoteToExternal;
}
fetchChannel();
@@ -150,13 +150,13 @@ function removePinnedNote(index: number) {
function save() {
const params = {
- name: name,
- description: description,
- bannerId: bannerId,
+ name: name.value,
+ description: description.value,
+ bannerId: bannerId.value,
pinnedNoteIds: pinnedNotes.value.map(x => x.id),
- color: color,
- isSensitive: isSensitive,
- allowRenoteToExternal: allowRenoteToExternal,
+ color: color.value,
+ isSensitive: isSensitive.value,
+ allowRenoteToExternal: allowRenoteToExternal.value,
};
if (props.channelId) {
@@ -172,7 +172,7 @@ function save() {
async function archive() {
const { canceled } = await os.confirm({
type: 'warning',
- title: i18n.t('channelArchiveConfirmTitle', { name: name }),
+ title: i18n.t('channelArchiveConfirmTitle', { name: name.value }),
text: i18n.ts.channelArchiveConfirmDescription,
});
@@ -188,17 +188,17 @@ async function archive() {
function setBannerImage(evt) {
selectFile(evt.currentTarget ?? evt.target, null).then(file => {
- bannerId = file.id;
+ bannerId.value = file.id;
});
}
function removeBannerImage() {
- bannerId = null;
+ bannerId.value = null;
}
-const headerActions = $computed(() => []);
+const headerActions = computed(() => []);
-const headerTabs = $computed(() => []);
+const headerTabs = computed(() => []);
definePageMetadata(computed(() => props.channelId ? {
title: i18n.ts._channel.edit,
diff --git a/packages/frontend/src/pages/channel.vue b/packages/frontend/src/pages/channel.vue
index 84463db0e1..a50965131f 100644
--- a/packages/frontend/src/pages/channel.vue
+++ b/packages/frontend/src/pages/channel.vue
@@ -12,7 +12,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<XChannelFollowButton :channel="channel" :full="true" :class="$style.subscribe"/>
<MkButton v-if="favorited" v-tooltip="i18n.ts.unfavorite" asLike class="button" rounded primary :class="$style.favorite" @click="unfavorite()"><i class="ph-star ph-bold ph-lg"></i></MkButton>
<MkButton v-else v-tooltip="i18n.ts.favorite" asLike class="button" rounded :class="$style.favorite" @click="favorite()"><i class="ph-star ph-bold ph-lg"></i></MkButton>
- <div :style="{ backgroundImage: channel.bannerUrl ? `url(${channel.bannerUrl})` : null }" :class="$style.banner">
+ <div :style="{ backgroundImage: channel.bannerUrl ? `url(${channel.bannerUrl})` : undefined }" :class="$style.banner">
<div :class="$style.bannerStatus">
<div><i class="ph-users ph-bold ph-lg ti-fw"></i><I18n :src="i18n.ts._channel.usersCount" tag="span" style="margin-left: 4px;"><template #n><b>{{ channel.usersCount }}</b></template></I18n></div>
<div><i class="ph-pencil ph-bold ph-lg ti-fw"></i><I18n :src="i18n.ts._channel.notesCount" tag="span" style="margin-left: 4px;"><template #n><b>{{ channel.notesCount }}</b></template></I18n></div>
@@ -27,7 +27,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkFoldableSection>
<template #header><i class="ph-push-pin ph-bold ph-lg ti-fw" style="margin-right: 0.5em;"></i>{{ i18n.ts.pinnedNotes }}</template>
- <div v-if="channel.pinnedNotes.length > 0" class="_gaps">
+ <div v-if="channel.pinnedNotes && channel.pinnedNotes.length > 0" class="_gaps">
<MkNote v-for="note in channel.pinnedNotes" :key="note.id" class="_panel" :note="note"/>
</div>
</MkFoldableSection>
@@ -38,7 +38,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<!-- スマホ・タブレットの場合、キーボードが表示されると投稿が見づらくなるので、デスクトップ場合のみ自動でフォーカスを当てる -->
<MkPostForm v-if="$i && defaultStore.reactiveState.showFixedPostFormInChannel.value" :channel="channel" class="post-form _panel" fixed :autofocus="deviceKind === 'desktop'"/>
- <MkTimeline :key="channelId" src="channel" :channel="channelId" @before="before" @after="after"/>
+ <MkTimeline :key="channelId" src="channel" :channel="channelId" @before="before" @after="after" @note="miLocalStorage.setItemAsJson(`channelLastReadedAt:${channel.id}`, Date.now())"/>
</div>
<div v-else-if="tab === 'featured'">
<MkNotes :pagination="featuredPagination"/>
@@ -68,7 +68,8 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { computed, watch } from 'vue';
+import { computed, watch, ref } from 'vue';
+import * as Misskey from 'misskey-js';
import MkPostForm from '@/components/MkPostForm.vue';
import MkTimeline from '@/components/MkTimeline.vue';
import XChannelFollowButton from '@/components/MkChannelFollowButton.vue';
@@ -86,6 +87,10 @@ import { defaultStore } from '@/store.js';
import MkNote from '@/components/MkNote.vue';
import MkInfo from '@/components/MkInfo.vue';
import MkFoldableSection from '@/components/MkFoldableSection.vue';
+import { PageHeaderItem } from '@/types/page-header.js';
+import { isSupportShare } from '@/scripts/navigator.js';
+import copyToClipboard from '@/scripts/copy-to-clipboard.js';
+import { miLocalStorage } from '@/local-storage.js';
const router = useRouter();
@@ -93,13 +98,13 @@ const props = defineProps<{
channelId: string;
}>();
-let tab = $ref('overview');
-let channel = $ref(null);
-let favorited = $ref(false);
-let searchQuery = $ref('');
-let searchPagination = $ref();
-let searchKey = $ref('');
-const featuredPagination = $computed(() => ({
+const tab = ref('overview');
+const channel = ref<Misskey.entities.Channel | null>(null);
+const favorited = ref(false);
+const searchQuery = ref('');
+const searchPagination = ref();
+const searchKey = ref('');
+const featuredPagination = computed(() => ({
endpoint: 'notes/featured' as const,
limit: 10,
params: {
@@ -108,89 +113,129 @@ const featuredPagination = $computed(() => ({
}));
watch(() => props.channelId, async () => {
- channel = await os.api('channels/show', {
+ channel.value = await os.api('channels/show', {
channelId: props.channelId,
});
- favorited = channel.isFavorited;
- if (favorited || channel.isFollowing) {
- tab = 'timeline';
+ favorited.value = channel.value.isFavorited ?? false;
+ if (favorited.value || channel.value.isFollowing) {
+ tab.value = 'timeline';
+ }
+
+ if ((favorited.value || channel.value.isFollowing) && channel.value.lastNotedAt) {
+ const lastReadedAt: number = miLocalStorage.getItemAsJson(`channelLastReadedAt:${channel.value.id}`) ?? 0;
+ const lastNotedAt = Date.parse(channel.value.lastNotedAt);
+
+ if (lastNotedAt > lastReadedAt) {
+ miLocalStorage.setItemAsJson(`channelLastReadedAt:${channel.value.id}`, lastNotedAt);
+ }
}
}, { immediate: true });
function edit() {
- router.push(`/channels/${channel.id}/edit`);
+ router.push(`/channels/${channel.value?.id}/edit`);
}
function openPostForm() {
os.post({
- channel,
+ channel: channel.value,
});
}
function favorite() {
+ if (!channel.value) return;
+
os.apiWithDialog('channels/favorite', {
- channelId: channel.id,
+ channelId: channel.value.id,
}).then(() => {
- favorited = true;
+ favorited.value = true;
});
}
async function unfavorite() {
+ if (!channel.value) return;
+
const confirm = await os.confirm({
type: 'warning',
text: i18n.ts.unfavoriteConfirm,
});
if (confirm.canceled) return;
os.apiWithDialog('channels/unfavorite', {
- channelId: channel.id,
+ channelId: channel.value.id,
}).then(() => {
- favorited = false;
+ favorited.value = false;
});
}
async function search() {
- const query = searchQuery.toString().trim();
+ if (!channel.value) return;
+
+ const query = searchQuery.value.toString().trim();
if (query == null) return;
- searchPagination = {
+ searchPagination.value = {
endpoint: 'notes/search',
limit: 10,
params: {
query: query,
- channelId: channel.id,
+ channelId: channel.value.id,
},
};
- searchKey = query;
+ searchKey.value = query;
}
-const headerActions = $computed(() => {
- if (channel && channel.userId) {
- const share = {
+const headerActions = computed(() => {
+ if (channel.value && channel.value.userId) {
+ const headerItems: PageHeaderItem[] = [];
+
+ headerItems.push({
icon: 'ph-share-network ph-bold ph-lg',
- text: i18n.ts.share,
+ text: i18n.ts.copyUrl,
handler: async (): Promise<void> => {
- navigator.share({
- title: channel.name,
- text: channel.description,
- url: `${url}/channels/${channel.id}`,
- });
+ if (!channel.value) {
+ console.warn('failed to copy channel URL. channel.value is null.');
+ return;
+ }
+ copyToClipboard(`${url}/channels/${channel.value.id}`);
+ os.success();
},
- };
+ });
+
+ if (isSupportShare()) {
+ headerItems.push({
+ icon: 'ph-share-network ph-bold ph-lg',
+ text: i18n.ts.share,
+ handler: async (): Promise<void> => {
+ if (!channel.value) {
+ console.warn('failed to share channel. channel.value is null.');
+ return;
+ }
+
+ navigator.share({
+ title: channel.value.name,
+ text: channel.value.description ?? undefined,
+ url: `${url}/channels/${channel.value.id}`,
+ });
+ },
+ });
+ }
+
+ if (($i && $i.id === channel.value.userId) || iAmModerator) {
+ headerItems.push({
+ icon: 'ph-gear ph-bold ph-lg',
+ text: i18n.ts.edit,
+ handler: edit,
+ });
+ }
- const canEdit = ($i && $i.id === channel.userId) || iAmModerator;
- return canEdit ? [share, {
- icon: 'ph-gear ph-bold ph-lg',
- text: i18n.ts.edit,
- handler: edit,
- }] : [share];
+ return headerItems.length > 0 ? headerItems : null;
} else {
return null;
}
});
-const headerTabs = $computed(() => [{
+const headerTabs = computed(() => [{
key: 'overview',
title: i18n.ts.overview,
icon: 'ph-info ph-bold ph-lg',
@@ -208,8 +253,8 @@ const headerTabs = $computed(() => [{
icon: 'ph-magnifying-glass ph-bold ph-lg',
}]);
-definePageMetadata(computed(() => channel ? {
- title: channel.name,
+definePageMetadata(computed(() => channel.value ? {
+ title: channel.value.name,
icon: 'ph-television ph-bold ph-lg',
} : null));
</script>
diff --git a/packages/frontend/src/pages/channels.vue b/packages/frontend/src/pages/channels.vue
index 07c205a001..182703f9da 100644
--- a/packages/frontend/src/pages/channels.vue
+++ b/packages/frontend/src/pages/channels.vue
@@ -50,7 +50,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { computed, onMounted } from 'vue';
+import { computed, onMounted, ref } from 'vue';
import MkChannelPreview from '@/components/MkChannelPreview.vue';
import MkChannelList from '@/components/MkChannelList.vue';
import MkPagination from '@/components/MkPagination.vue';
@@ -69,15 +69,15 @@ const props = defineProps<{
type?: string;
}>();
-let key = $ref('');
-let tab = $ref('featured');
-let searchQuery = $ref('');
-let searchType = $ref('nameAndDescription');
-let channelPagination = $ref();
+const key = ref('');
+const tab = ref('featured');
+const searchQuery = ref('');
+const searchType = ref('nameAndDescription');
+const channelPagination = ref();
onMounted(() => {
- searchQuery = props.query ?? '';
- searchType = props.type ?? 'nameAndDescription';
+ searchQuery.value = props.query ?? '';
+ searchType.value = props.type ?? 'nameAndDescription';
});
const featuredPagination = {
@@ -99,35 +99,35 @@ const ownedPagination = {
};
async function search() {
- const query = searchQuery.toString().trim();
+ const query = searchQuery.value.toString().trim();
if (query == null) return;
- const type = searchType.toString().trim();
+ const type = searchType.value.toString().trim();
- channelPagination = {
+ channelPagination.value = {
endpoint: 'channels/search',
limit: 10,
params: {
- query: searchQuery,
+ query: searchQuery.value,
type: type,
},
};
- key = query + type;
+ key.value = query + type;
}
function create() {
router.push('/channels/new');
}
-const headerActions = $computed(() => [{
+const headerActions = computed(() => [{
icon: 'ph-plus ph-bold ph-lg',
text: i18n.ts.create,
handler: create,
}]);
-const headerTabs = $computed(() => [{
+const headerTabs = computed(() => [{
key: 'search',
title: i18n.ts.search,
icon: 'ph-magnifying-glass ph-bold ph-lg',
diff --git a/packages/frontend/src/pages/clip.vue b/packages/frontend/src/pages/clip.vue
index 37b7870dd4..041cc0a204 100644
--- a/packages/frontend/src/pages/clip.vue
+++ b/packages/frontend/src/pages/clip.vue
@@ -26,7 +26,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { computed, watch, provide } from 'vue';
+import { computed, watch, provide, ref } from 'vue';
import * as Misskey from 'misskey-js';
import MkNotes from '@/components/MkNotes.vue';
import { $i } from '@/account.js';
@@ -36,13 +36,15 @@ import { definePageMetadata } from '@/scripts/page-metadata.js';
import { url } from '@/config.js';
import MkButton from '@/components/MkButton.vue';
import { clipsCache } from '@/cache';
+import { isSupportShare } from '@/scripts/navigator.js';
+import copyToClipboard from '@/scripts/copy-to-clipboard.js';
const props = defineProps<{
clipId: string,
}>();
-let clip: Misskey.entities.Clip = $ref<Misskey.entities.Clip>();
-let favorited = $ref(false);
+const clip = ref<Misskey.entities.Clip | null>(null);
+const favorited = ref(false);
const pagination = {
endpoint: 'clips/notes' as const,
limit: 10,
@@ -51,24 +53,24 @@ const pagination = {
})),
};
-const isOwned: boolean | null = $computed<boolean | null>(() => $i && clip && ($i.id === clip.userId));
+const isOwned = computed<boolean | null>(() => $i && clip.value && ($i.id === clip.value.userId));
watch(() => props.clipId, async () => {
- clip = await os.api('clips/show', {
+ clip.value = await os.api('clips/show', {
clipId: props.clipId,
});
- favorited = clip.isFavorited;
+ favorited.value = clip.value.isFavorited;
}, {
immediate: true,
});
-provide('currentClip', $$(clip));
+provide('currentClip', clip);
function favorite() {
os.apiWithDialog('clips/favorite', {
clipId: props.clipId,
}).then(() => {
- favorited = true;
+ favorited.value = true;
});
}
@@ -81,50 +83,58 @@ async function unfavorite() {
os.apiWithDialog('clips/unfavorite', {
clipId: props.clipId,
}).then(() => {
- favorited = false;
+ favorited.value = false;
});
}
-const headerActions = $computed(() => clip && isOwned ? [{
+const headerActions = computed(() => clip.value && isOwned.value ? [{
icon: 'ph-pencil ph-bold ph-lg',
text: i18n.ts.edit,
handler: async (): Promise<void> => {
- const { canceled, result } = await os.form(clip.name, {
+ const { canceled, result } = await os.form(clip.value.name, {
name: {
type: 'string',
label: i18n.ts.name,
- default: clip.name,
+ default: clip.value.name,
},
description: {
type: 'string',
required: false,
multiline: true,
+ treatAsMfm: true,
label: i18n.ts.description,
- default: clip.description,
+ default: clip.value.description,
},
isPublic: {
type: 'boolean',
label: i18n.ts.public,
- default: clip.isPublic,
+ default: clip.value.isPublic,
},
});
if (canceled) return;
os.apiWithDialog('clips/update', {
- clipId: clip.id,
+ clipId: clip.value.id,
...result,
});
clipsCache.delete();
},
-}, ...(clip.isPublic ? [{
+}, ...(clip.value.isPublic ? [{
+ icon: 'ph-share-network ph-bold ph-lg',
+ text: i18n.ts.copyUrl,
+ handler: async (): Promise<void> => {
+ copyToClipboard(`${url}/clips/${clip.value.id}`);
+ os.success();
+ },
+}] : []), ...(clip.value.isPublic && isSupportShare() ? [{
icon: 'ph-share-network ph-bold ph-lg',
text: i18n.ts.share,
handler: async (): Promise<void> => {
navigator.share({
- title: clip.name,
- text: clip.description,
- url: `${url}/clips/${clip.id}`,
+ title: clip.value.name,
+ text: clip.value.description,
+ url: `${url}/clips/${clip.value.id}`,
});
},
}] : []), {
@@ -134,20 +144,20 @@ const headerActions = $computed(() => clip && isOwned ? [{
handler: async (): Promise<void> => {
const { canceled } = await os.confirm({
type: 'warning',
- text: i18n.t('deleteAreYouSure', { x: clip.name }),
+ text: i18n.t('deleteAreYouSure', { x: clip.value.name }),
});
if (canceled) return;
await os.apiWithDialog('clips/delete', {
- clipId: clip.id,
+ clipId: clip.value.id,
});
clipsCache.delete();
},
}] : null);
-definePageMetadata(computed(() => clip ? {
- title: clip.name,
+definePageMetadata(computed(() => clip.value ? {
+ title: clip.value.name,
icon: 'ph-paperclip ph-bold ph-lg',
} : null));
</script>
diff --git a/packages/frontend/src/pages/custom-emojis-manager.vue b/packages/frontend/src/pages/custom-emojis-manager.vue
index 0c5825091a..9f68b6b485 100644
--- a/packages/frontend/src/pages/custom-emojis-manager.vue
+++ b/packages/frontend/src/pages/custom-emojis-manager.vue
@@ -80,7 +80,7 @@ import MkInput from '@/components/MkInput.vue';
import MkPagination from '@/components/MkPagination.vue';
import MkSwitch from '@/components/MkSwitch.vue';
import FormSplit from '@/components/form/split.vue';
-import { selectFile, selectFiles } from '@/scripts/select-file.js';
+import { selectFile } from '@/scripts/select-file.js';
import * as os from '@/os.js';
import { i18n } from '@/i18n.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';
@@ -155,7 +155,7 @@ const edit = (emoji) => {
}, 'closed');
};
-const im = (emoji) => {
+const importEmoji = (emoji) => {
os.apiWithDialog('admin/emoji/copy', {
emojiId: emoji.id,
});
@@ -169,13 +169,13 @@ const remoteMenu = (emoji, ev: MouseEvent) => {
{
text: i18n.ts.import,
icon: 'ph-plus ph-bold ph-lg',
- action: () => { im(emoji); },
+ action: () => { importEmoji(emoji); },
},
{
text: i18n.ts.delete,
- icon: "ph-trash ph-bold ph-lg",
+ icon: 'ph-trash ph-bold ph-lg',
action: () => {
- os.apiWithDialog("admin/emoji/delete", {
+ os.apiWithDialog('admin/emoji/delete', {
id: emoji.id,
});
},
@@ -296,7 +296,7 @@ const delBulk = async () => {
emojisPaginationComponent.value.reload();
};
-const headerActions = $computed(() => [{
+const headerActions = computed(() => [{
asFullButton: true,
icon: 'ph-plus ph-bold ph-lg',
text: i18n.ts.addEmoji,
@@ -306,7 +306,7 @@ const headerActions = $computed(() => [{
handler: menu,
}]);
-const headerTabs = $computed(() => [{
+const headerTabs = computed(() => [{
key: 'local',
title: i18n.ts.local,
}, {
diff --git a/packages/frontend/src/pages/drive.vue b/packages/frontend/src/pages/drive.vue
index b92dc8b470..7c88abc167 100644
--- a/packages/frontend/src/pages/drive.vue
+++ b/packages/frontend/src/pages/drive.vue
@@ -10,19 +10,19 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { computed } from 'vue';
+import { computed, ref } from 'vue';
import XDrive from '@/components/MkDrive.vue';
import { i18n } from '@/i18n.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';
-let folder = $ref(null);
+const folder = ref(null);
-const headerActions = $computed(() => []);
+const headerActions = computed(() => []);
-const headerTabs = $computed(() => []);
+const headerTabs = computed(() => []);
definePageMetadata(computed(() => ({
- title: folder ? folder.name : i18n.ts.drive,
+ title: folder.value ? folder.value.name : i18n.ts.drive,
icon: 'ph-cloud ph-bold ph-lg',
hideHeader: true,
})));
diff --git a/packages/frontend/src/pages/emoji-edit-dialog.vue b/packages/frontend/src/pages/emoji-edit-dialog.vue
index b4ae855d5d..ce6b5ae5f0 100644
--- a/packages/frontend/src/pages/emoji-edit-dialog.vue
+++ b/packages/frontend/src/pages/emoji-edit-dialog.vue
@@ -74,7 +74,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { computed, watch } from 'vue';
+import { computed, watch, ref } from 'vue';
import * as Misskey from 'misskey-js';
import MkModalWindow from '@/components/MkModalWindow.vue';
import MkButton from '@/components/MkButton.vue';
@@ -85,29 +85,29 @@ import * as os from '@/os.js';
import { i18n } from '@/i18n.js';
import { customEmojiCategories } from '@/custom-emojis.js';
import MkSwitch from '@/components/MkSwitch.vue';
-import { selectFile, selectFiles } from '@/scripts/select-file.js';
+import { selectFile } from '@/scripts/select-file.js';
import MkRolePreview from '@/components/MkRolePreview.vue';
const props = defineProps<{
emoji?: any,
}>();
-let dialog = $ref(null);
-let name: string = $ref(props.emoji ? props.emoji.name : '');
-let category: string = $ref(props.emoji ? props.emoji.category : '');
-let aliases: string = $ref(props.emoji ? props.emoji.aliases.join(' ') : '');
-let license: string = $ref(props.emoji ? (props.emoji.license ?? '') : '');
-let isSensitive = $ref(props.emoji ? props.emoji.isSensitive : false);
-let localOnly = $ref(props.emoji ? props.emoji.localOnly : false);
-let roleIdsThatCanBeUsedThisEmojiAsReaction = $ref(props.emoji ? props.emoji.roleIdsThatCanBeUsedThisEmojiAsReaction : []);
-let rolesThatCanBeUsedThisEmojiAsReaction = $ref([]);
-let file = $ref<Misskey.entities.DriveFile>();
+const dialog = ref(null);
+const name = ref<string>(props.emoji ? props.emoji.name : '');
+const category = ref<string>(props.emoji ? props.emoji.category : '');
+const aliases = ref<string>(props.emoji ? props.emoji.aliases.join(' ') : '');
+const license = ref<string>(props.emoji ? (props.emoji.license ?? '') : '');
+const isSensitive = ref(props.emoji ? props.emoji.isSensitive : false);
+const localOnly = ref(props.emoji ? props.emoji.localOnly : false);
+const roleIdsThatCanBeUsedThisEmojiAsReaction = ref(props.emoji ? props.emoji.roleIdsThatCanBeUsedThisEmojiAsReaction : []);
+const rolesThatCanBeUsedThisEmojiAsReaction = ref([]);
+const file = ref<Misskey.entities.DriveFile>();
-watch($$(roleIdsThatCanBeUsedThisEmojiAsReaction), async () => {
- rolesThatCanBeUsedThisEmojiAsReaction = (await Promise.all(roleIdsThatCanBeUsedThisEmojiAsReaction.map((id) => os.api('admin/roles/show', { roleId: id }).catch(() => null)))).filter(x => x != null);
+watch(roleIdsThatCanBeUsedThisEmojiAsReaction, async () => {
+ rolesThatCanBeUsedThisEmojiAsReaction.value = (await Promise.all(roleIdsThatCanBeUsedThisEmojiAsReaction.value.map((id) => os.api('admin/roles/show', { roleId: id }).catch(() => null)))).filter(x => x != null);
}, { immediate: true });
-const imgUrl = computed(() => file ? file.url : props.emoji ? `/emoji/${props.emoji.name}.webp` : null);
+const imgUrl = computed(() => file.value ? file.value.url : props.emoji ? `/emoji/${props.emoji.name}.webp` : null);
const emit = defineEmits<{
(ev: 'done', v: { deleted?: boolean; updated?: any; created?: any }): void,
@@ -115,42 +115,42 @@ const emit = defineEmits<{
}>();
async function changeImage(ev) {
- file = await selectFile(ev.currentTarget ?? ev.target, null);
- const candidate = file.name.replace(/\.(.+)$/, '');
+ file.value = await selectFile(ev.currentTarget ?? ev.target, null);
+ const candidate = file.value.name.replace(/\.(.+)$/, '');
if (candidate.match(/^[a-z0-9_]+$/)) {
- name = candidate;
+ name.value = candidate;
}
}
async function addRole() {
const roles = await os.api('admin/roles/list');
- const currentRoleIds = rolesThatCanBeUsedThisEmojiAsReaction.map(x => x.id);
+ const currentRoleIds = rolesThatCanBeUsedThisEmojiAsReaction.value.map(x => x.id);
const { canceled, result: role } = await os.select({
items: roles.filter(r => r.isPublic).filter(r => !currentRoleIds.includes(r.id)).map(r => ({ text: r.name, value: r })),
});
if (canceled) return;
- rolesThatCanBeUsedThisEmojiAsReaction.push(role);
+ rolesThatCanBeUsedThisEmojiAsReaction.value.push(role);
}
async function removeRole(role, ev) {
- rolesThatCanBeUsedThisEmojiAsReaction = rolesThatCanBeUsedThisEmojiAsReaction.filter(x => x.id !== role.id);
+ rolesThatCanBeUsedThisEmojiAsReaction.value = rolesThatCanBeUsedThisEmojiAsReaction.value.filter(x => x.id !== role.id);
}
async function done() {
const params = {
- name,
- category: category === '' ? null : category,
- aliases: aliases.split(' ').filter(x => x !== ''),
- license: license === '' ? null : license,
- isSensitive,
- localOnly,
- roleIdsThatCanBeUsedThisEmojiAsReaction: rolesThatCanBeUsedThisEmojiAsReaction.map(x => x.id),
+ name: name.value,
+ category: category.value === '' ? null : category.value,
+ aliases: aliases.value.split(' ').filter(x => x !== ''),
+ license: license.value === '' ? null : license.value,
+ isSensitive: isSensitive.value,
+ localOnly: localOnly.value,
+ roleIdsThatCanBeUsedThisEmojiAsReaction: rolesThatCanBeUsedThisEmojiAsReaction.value.map(x => x.id),
};
- if (file) {
- params.fileId = file.id;
+ if (file.value) {
+ params.fileId = file.value.id;
}
if (props.emoji) {
@@ -166,7 +166,7 @@ async function done() {
},
});
- dialog.close();
+ dialog.value.close();
} else {
const created = await os.apiWithDialog('admin/emoji/add', params);
@@ -174,14 +174,14 @@ async function done() {
created: created,
});
- dialog.close();
+ dialog.value.close();
}
}
async function del() {
const { canceled } = await os.confirm({
type: 'warning',
- text: i18n.t('removeAreYouSure', { x: name }),
+ text: i18n.t('removeAreYouSure', { x: name.value }),
});
if (canceled) return;
@@ -191,7 +191,7 @@ async function del() {
emit('done', {
deleted: true,
});
- dialog.close();
+ dialog.value.close();
});
}
</script>
diff --git a/packages/frontend/src/pages/emojis.emoji.vue b/packages/frontend/src/pages/emojis.emoji.vue
index f787b12097..d94fe96fa2 100644
--- a/packages/frontend/src/pages/emojis.emoji.vue
+++ b/packages/frontend/src/pages/emojis.emoji.vue
@@ -46,7 +46,7 @@ function menu(ev) {
os.apiGet('emoji', { name: props.emoji.name }).then(res => {
os.alert({
type: 'info',
- text: `License: ${res.license}`,
+ text: `Name: ${res.name}\nAliases: ${res.aliases.join(' ')}\nCategory: ${res.category}\nisSensitive: ${res.isSensitive}\nlocalOnly: ${res.localOnly}\nLicense: ${res.license}\nURL: ${res.url}`,
});
});
},
diff --git a/packages/frontend/src/pages/explore.featured.vue b/packages/frontend/src/pages/explore.featured.vue
index a36d1b3bda..000371528e 100644
--- a/packages/frontend/src/pages/explore.featured.vue
+++ b/packages/frontend/src/pages/explore.featured.vue
@@ -15,6 +15,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
+import { ref } from 'vue';
import MkNotes from '@/components/MkNotes.vue';
import MkTab from '@/components/MkTab.vue';
import { i18n } from '@/i18n.js';
@@ -30,5 +31,5 @@ const paginationForPolls = {
offsetMode: true,
};
-let tab = $ref('notes');
+const tab = ref('notes');
</script>
diff --git a/packages/frontend/src/pages/explore.roles.vue b/packages/frontend/src/pages/explore.roles.vue
index 995ccd777c..929da19426 100644
--- a/packages/frontend/src/pages/explore.roles.vue
+++ b/packages/frontend/src/pages/explore.roles.vue
@@ -12,14 +12,14 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { } from 'vue';
+import { ref } from 'vue';
import MkRolePreview from '@/components/MkRolePreview.vue';
import * as os from '@/os.js';
-let roles = $ref();
+const roles = ref();
os.api('roles/list').then(res => {
- roles = res.filter(x => x.target === 'manual').sort((a, b) => b.displayOrder - a.displayOrder);
+ roles.value = res.filter(x => x.target === 'manual').sort((a, b) => b.displayOrder - a.displayOrder);
});
</script>
diff --git a/packages/frontend/src/pages/explore.users.vue b/packages/frontend/src/pages/explore.users.vue
index f7215def62..741a3bc219 100644
--- a/packages/frontend/src/pages/explore.users.vue
+++ b/packages/frontend/src/pages/explore.users.vue
@@ -63,7 +63,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { watch } from 'vue';
+import { watch, ref, shallowRef, computed } from 'vue';
import MkUserList from '@/components/MkUserList.vue';
import MkFoldableSection from '@/components/MkFoldableSection.vue';
import MkTab from '@/components/MkTab.vue';
@@ -74,16 +74,16 @@ const props = defineProps<{
tag?: string;
}>();
-let origin = $ref('local');
-let tagsEl = $shallowRef<InstanceType<typeof MkFoldableSection>>();
-let tagsLocal = $ref([]);
-let tagsRemote = $ref([]);
+const origin = ref('local');
+const tagsEl = shallowRef<InstanceType<typeof MkFoldableSection>>();
+const tagsLocal = ref([]);
+const tagsRemote = ref([]);
watch(() => props.tag, () => {
- if (tagsEl) tagsEl.toggleContent(props.tag == null);
+ if (tagsEl.value) tagsEl.value.toggleContent(props.tag == null);
});
-const tagUsers = $computed(() => ({
+const tagUsers = computed(() => ({
endpoint: 'hashtags/users' as const,
limit: 30,
params: {
@@ -127,13 +127,13 @@ os.api('hashtags/list', {
attachedToLocalUserOnly: true,
limit: 30,
}).then(tags => {
- tagsLocal = tags;
+ tagsLocal.value = tags;
});
os.api('hashtags/list', {
sort: '+attachedRemoteUsers',
attachedToRemoteUserOnly: true,
limit: 30,
}).then(tags => {
- tagsRemote = tags;
+ tagsRemote.value = tags;
});
</script>
diff --git a/packages/frontend/src/pages/explore.vue b/packages/frontend/src/pages/explore.vue
index 8b5af28b12..9693e26598 100644
--- a/packages/frontend/src/pages/explore.vue
+++ b/packages/frontend/src/pages/explore.vue
@@ -21,7 +21,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { computed, watch } from 'vue';
+import { computed, watch, ref, shallowRef } from 'vue';
import XFeatured from './explore.featured.vue';
import XUsers from './explore.users.vue';
import XRoles from './explore.roles.vue';
@@ -36,16 +36,16 @@ const props = withDefaults(defineProps<{
initialTab: 'featured',
});
-let tab = $ref(props.initialTab);
-let tagsEl = $shallowRef<InstanceType<typeof MkFoldableSection>>();
+const tab = ref(props.initialTab);
+const tagsEl = shallowRef<InstanceType<typeof MkFoldableSection>>();
watch(() => props.tag, () => {
- if (tagsEl) tagsEl.toggleContent(props.tag == null);
+ if (tagsEl.value) tagsEl.value.toggleContent(props.tag == null);
});
-const headerActions = $computed(() => []);
+const headerActions = computed(() => []);
-const headerTabs = $computed(() => [{
+const headerTabs = computed(() => [{
key: 'featured',
icon: 'ph-lightning ph-bold ph-lg',
title: i18n.ts.featured,
diff --git a/packages/frontend/src/pages/flash/flash-edit.vue b/packages/frontend/src/pages/flash/flash-edit.vue
index ce25ad63a8..a4c2c0bc37 100644
--- a/packages/frontend/src/pages/flash/flash-edit.vue
+++ b/packages/frontend/src/pages/flash/flash-edit.vue
@@ -15,9 +15,9 @@ SPDX-License-Identifier: AGPL-3.0-only
<template #label>{{ i18n.ts._play.summary }}</template>
</MkTextarea>
<MkButton primary @click="selectPreset">{{ i18n.ts.selectFromPresets }}<i class="ph-caret-down ph-bold ph-lg"></i></MkButton>
- <MkTextarea v-model="script" class="_monospace" tall spellcheck="false">
+ <MkCodeEditor v-model="script" lang="is">
<template #label>{{ i18n.ts._play.script }}</template>
- </MkTextarea>
+ </MkCodeEditor>
<div class="_buttons">
<MkButton primary @click="save"><i class="ph-check ph-bold ph-lg"></i> {{ i18n.ts.save }}</MkButton>
<MkButton @click="show"><i class="ph-eye ph-bold ph-lg"></i> {{ i18n.ts.show }}</MkButton>
@@ -34,12 +34,13 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { computed } from 'vue';
+import { computed, ref } from 'vue';
import MkButton from '@/components/MkButton.vue';
import * as os from '@/os.js';
import { i18n } from '@/i18n.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';
import MkTextarea from '@/components/MkTextarea.vue';
+import MkCodeEditor from '@/components/MkCodeEditor.vue';
import MkInput from '@/components/MkInput.vue';
import MkSelect from '@/components/MkSelect.vue';
import { useRouter } from '@/router.js';
@@ -363,79 +364,79 @@ const props = defineProps<{
id?: string;
}>();
-let flash = $ref(null);
-let visibility = $ref('public');
+const flash = ref(null);
+const visibility = ref('public');
if (props.id) {
- flash = await os.api('flash/show', {
+ flash.value = await os.api('flash/show', {
flashId: props.id,
});
}
-let title = $ref(flash?.title ?? 'New Play');
-let summary = $ref(flash?.summary ?? '');
-let permissions = $ref(flash?.permissions ?? []);
-let script = $ref(flash?.script ?? PRESET_DEFAULT);
+const title = ref(flash.value?.title ?? 'New Play');
+const summary = ref(flash.value?.summary ?? '');
+const permissions = ref(flash.value?.permissions ?? []);
+const script = ref(flash.value?.script ?? PRESET_DEFAULT);
function selectPreset(ev: MouseEvent) {
os.popupMenu([{
text: 'Omikuji',
action: () => {
- script = PRESET_OMIKUJI;
+ script.value = PRESET_OMIKUJI;
},
}, {
text: 'Shuffle',
action: () => {
- script = PRESET_SHUFFLE;
+ script.value = PRESET_SHUFFLE;
},
}, {
text: 'Quiz',
action: () => {
- script = PRESET_QUIZ;
+ script.value = PRESET_QUIZ;
},
}, {
text: 'Timeline viewer',
action: () => {
- script = PRESET_TIMELINE;
+ script.value = PRESET_TIMELINE;
},
}], ev.currentTarget ?? ev.target);
}
async function save() {
- if (flash) {
+ if (flash.value) {
os.apiWithDialog('flash/update', {
flashId: props.id,
- title,
- summary,
- permissions,
- script,
- visibility,
+ title: title.value,
+ summary: summary.value,
+ permissions: permissions.value,
+ script: script.value,
+ visibility: visibility.value,
});
} else {
const created = await os.apiWithDialog('flash/create', {
- title,
- summary,
- permissions,
- script,
+ title: title.value,
+ summary: summary.value,
+ permissions: permissions.value,
+ script: script.value,
});
router.push('/play/' + created.id + '/edit');
}
}
function show() {
- if (flash == null) {
+ if (flash.value == null) {
os.alert({
text: 'Please save',
});
} else {
- os.pageWindow(`/play/${flash.id}`);
+ os.pageWindow(`/play/${flash.value.id}`);
}
}
async function del() {
const { canceled } = await os.confirm({
type: 'warning',
- text: i18n.t('deleteAreYouSure', { x: flash.title }),
+ text: i18n.t('deleteAreYouSure', { x: flash.value.title }),
});
if (canceled) return;
@@ -445,12 +446,12 @@ async function del() {
router.push('/play');
}
-const headerActions = $computed(() => []);
+const headerActions = computed(() => []);
-const headerTabs = $computed(() => []);
+const headerTabs = computed(() => []);
-definePageMetadata(computed(() => flash ? {
- title: i18n.ts._play.edit + ': ' + flash.title,
+definePageMetadata(computed(() => flash.value ? {
+ title: i18n.ts._play.edit + ': ' + flash.value.title,
} : {
title: i18n.ts._play.new,
}));
diff --git a/packages/frontend/src/pages/flash/flash-index.vue b/packages/frontend/src/pages/flash/flash-index.vue
index 0b4dd13911..2b9346fcac 100644
--- a/packages/frontend/src/pages/flash/flash-index.vue
+++ b/packages/frontend/src/pages/flash/flash-index.vue
@@ -38,7 +38,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { computed } from 'vue';
+import { computed, ref } from 'vue';
import MkFlashPreview from '@/components/MkFlashPreview.vue';
import MkPagination from '@/components/MkPagination.vue';
import MkButton from '@/components/MkButton.vue';
@@ -48,7 +48,7 @@ import { definePageMetadata } from '@/scripts/page-metadata.js';
const router = useRouter();
-let tab = $ref('featured');
+const tab = ref('featured');
const featuredFlashsPagination = {
endpoint: 'flash/featured' as const,
@@ -67,13 +67,13 @@ function create() {
router.push('/play/new');
}
-const headerActions = $computed(() => [{
+const headerActions = computed(() => [{
icon: 'ph-plus ph-bold ph-lg',
text: i18n.ts.create,
handler: create,
}]);
-const headerTabs = $computed(() => [{
+const headerTabs = computed(() => [{
key: 'featured',
title: i18n.ts._play.featured,
icon: 'ph-fire ph-bold ph-lg',
diff --git a/packages/frontend/src/pages/flash/flash.vue b/packages/frontend/src/pages/flash/flash.vue
index fd03ac6579..c7ffd5c966 100644
--- a/packages/frontend/src/pages/flash/flash.vue
+++ b/packages/frontend/src/pages/flash/flash.vue
@@ -18,7 +18,8 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkButton v-if="flash.isLiked" v-tooltip="i18n.ts.unlike" asLike class="button" rounded primary @click="unlike()"><i class="ph-heart ph-bold ph-lg"></i><span v-if="flash.likedCount > 0" style="margin-left: 6px;">{{ flash.likedCount }}</span></MkButton>
<MkButton v-else v-tooltip="i18n.ts.like" asLike class="button" rounded @click="like()"><i class="ph-heart ph-bold ph-lg"></i><span v-if="flash.likedCount > 0" style="margin-left: 6px;">{{ flash.likedCount }}</span></MkButton>
<MkButton v-tooltip="i18n.ts.shareWithNote" class="button" rounded @click="shareWithNote"><i class="ph-repeat ph-bold ph-lg ti-fw"></i></MkButton>
- <MkButton v-tooltip="i18n.ts.share" class="button" rounded @click="share"><i class="ph-share-network ph-bold ph-lg ti-fw"></i></MkButton>
+ <MkButton v-tooltip="i18n.ts.copyLink" class="button" rounded @click="copyLink"><i class="ph-share-network ph-bold ph-lg ti-fw"></i></MkButton>
+ <MkButton v-if="isSupportShare()" v-tooltip="i18n.ts.share" class="button" rounded @click="share"><i class="ph-share-network ph-bold ph-lg ti-fw"></i></MkButton>
</div>
</div>
<div v-else :class="$style.ready">
@@ -56,7 +57,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { computed, onDeactivated, onUnmounted, Ref, ref, watch } from 'vue';
+import { computed, onDeactivated, onUnmounted, Ref, ref, watch, shallowRef } from 'vue';
import { Interpreter, Parser, values } from '@syuilo/aiscript';
import MkButton from '@/components/MkButton.vue';
import * as os from '@/os.js';
@@ -70,45 +71,52 @@ import MkFolder from '@/components/MkFolder.vue';
import MkCode from '@/components/MkCode.vue';
import { defaultStore } from '@/store.js';
import { $i } from '@/account.js';
+import { isSupportShare } from '@/scripts/navigator.js';
+import copyToClipboard from '@/scripts/copy-to-clipboard.js';
const props = defineProps<{
id: string;
}>();
-let flash = $ref(null);
-let error = $ref(null);
+const flash = ref(null);
+const error = ref(null);
function fetchFlash() {
- flash = null;
+ flash.value = null;
os.api('flash/show', {
flashId: props.id,
}).then(_flash => {
- flash = _flash;
+ flash.value = _flash;
}).catch(err => {
- error = err;
+ error.value = err;
});
}
+function copyLink() {
+ copyToClipboard(`${url}/play/${flash.value.id}`);
+ os.success();
+}
+
function share() {
navigator.share({
- title: flash.title,
- text: flash.summary,
- url: `${url}/play/${flash.id}`,
+ title: flash.value.title,
+ text: flash.value.summary,
+ url: `${url}/play/${flash.value.id}`,
});
}
function shareWithNote() {
os.post({
- initialText: `${flash.title} ${url}/play/${flash.id}`,
+ initialText: `${flash.value.title} ${url}/play/${flash.value.id}`,
});
}
function like() {
os.apiWithDialog('flash/like', {
- flashId: flash.id,
+ flashId: flash.value.id,
}).then(() => {
- flash.isLiked = true;
- flash.likedCount++;
+ flash.value.isLiked = true;
+ flash.value.likedCount++;
});
}
@@ -119,10 +127,10 @@ async function unlike() {
});
if (confirm.canceled) return;
os.apiWithDialog('flash/unlike', {
- flashId: flash.id,
+ flashId: flash.value.id,
}).then(() => {
- flash.isLiked = false;
- flash.likedCount--;
+ flash.value.isLiked = false;
+ flash.value.likedCount--;
});
}
@@ -130,39 +138,35 @@ watch(() => props.id, fetchFlash, { immediate: true });
const parser = new Parser();
-let started = $ref(false);
-let aiscript = $shallowRef<Interpreter | null>(null);
+const started = ref(false);
+const aiscript = shallowRef<Interpreter | null>(null);
const root = ref<AsUiRoot>();
-const components: Ref<AsUiComponent>[] = $ref([]);
+const components = ref<Ref<AsUiComponent>[]>([]);
function start() {
- started = true;
+ started.value = true;
run();
}
async function run() {
- if (aiscript) aiscript.abort();
+ if (aiscript.value) aiscript.value.abort();
- aiscript = new Interpreter({
+ aiscript.value = new Interpreter({
...createAiScriptEnv({
- storageKey: 'flash:' + flash.id,
+ storageKey: 'flash:' + flash.value.id,
}),
- ...registerAsUiLib(components, (_root) => {
+ ...registerAsUiLib(components.value, (_root) => {
root.value = _root.value;
}),
- THIS_ID: values.STR(flash.id),
- THIS_URL: values.STR(`${url}/play/${flash.id}`),
+ THIS_ID: values.STR(flash.value.id),
+ THIS_URL: values.STR(`${url}/play/${flash.value.id}`),
}, {
in: (q) => {
return new Promise(ok => {
os.inputText({
title: q,
- }).then(({ canceled, result: a }) => {
- if (canceled) {
- ok('');
- } else {
- ok(a);
- }
+ }).then(({ result: a }) => {
+ ok(a ?? '');
});
});
},
@@ -176,7 +180,7 @@ async function run() {
let ast;
try {
- ast = parser.parse(flash.script);
+ ast = parser.parse(flash.value.script);
} catch (err) {
os.alert({
type: 'error',
@@ -185,7 +189,7 @@ async function run() {
return;
}
try {
- await aiscript.exec(ast);
+ await aiscript.value.exec(ast);
} catch (err) {
os.alert({
type: 'error',
@@ -196,24 +200,24 @@ async function run() {
}
onDeactivated(() => {
- if (aiscript) aiscript.abort();
+ if (aiscript.value) aiscript.value.abort();
});
onUnmounted(() => {
- if (aiscript) aiscript.abort();
+ if (aiscript.value) aiscript.value.abort();
});
-const headerActions = $computed(() => []);
+const headerActions = computed(() => []);
-const headerTabs = $computed(() => []);
+const headerTabs = computed(() => []);
-definePageMetadata(computed(() => flash ? {
- title: flash.title,
- avatar: flash.user,
- path: `/play/${flash.id}`,
+definePageMetadata(computed(() => flash.value ? {
+ title: flash.value.title,
+ avatar: flash.value.user,
+ path: `/play/${flash.value.id}`,
share: {
- title: flash.title,
- text: flash.summary,
+ title: flash.value.title,
+ text: flash.value.summary,
},
} : null));
</script>
diff --git a/packages/frontend/src/pages/follow-requests.vue b/packages/frontend/src/pages/follow-requests.vue
index 7546804a7b..d750664221 100644
--- a/packages/frontend/src/pages/follow-requests.vue
+++ b/packages/frontend/src/pages/follow-requests.vue
@@ -65,9 +65,9 @@ function reject(user) {
});
}
-const headerActions = $computed(() => []);
+const headerActions = computed(() => []);
-const headerTabs = $computed(() => []);
+const headerTabs = computed(() => []);
definePageMetadata(computed(() => ({
title: i18n.ts.followRequests,
diff --git a/packages/frontend/src/pages/gallery/edit.vue b/packages/frontend/src/pages/gallery/edit.vue
index 5daaca774c..ef6286f27f 100644
--- a/packages/frontend/src/pages/gallery/edit.vue
+++ b/packages/frontend/src/pages/gallery/edit.vue
@@ -38,7 +38,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { computed, watch } from 'vue';
+import { computed, watch, ref } from 'vue';
import MkButton from '@/components/MkButton.vue';
import MkInput from '@/components/MkInput.vue';
import MkTextarea from '@/components/MkTextarea.vue';
@@ -56,38 +56,38 @@ const props = defineProps<{
postId?: string;
}>();
-let init = $ref(null);
-let files = $ref([]);
-let description = $ref(null);
-let title = $ref(null);
-let isSensitive = $ref(false);
+const init = ref(null);
+const files = ref([]);
+const description = ref(null);
+const title = ref(null);
+const isSensitive = ref(false);
function selectFile(evt) {
selectFiles(evt.currentTarget ?? evt.target, null).then(selected => {
- files = files.concat(selected);
+ files.value = files.value.concat(selected);
});
}
function remove(file) {
- files = files.filter(f => f.id !== file.id);
+ files.value = files.value.filter(f => f.id !== file.id);
}
async function save() {
if (props.postId) {
await os.apiWithDialog('gallery/posts/update', {
postId: props.postId,
- title: title,
- description: description,
- fileIds: files.map(file => file.id),
- isSensitive: isSensitive,
+ title: title.value,
+ description: description.value,
+ fileIds: files.value.map(file => file.id),
+ isSensitive: isSensitive.value,
});
router.push(`/gallery/${props.postId}`);
} else {
const created = await os.apiWithDialog('gallery/posts/create', {
- title: title,
- description: description,
- fileIds: files.map(file => file.id),
- isSensitive: isSensitive,
+ title: title.value,
+ description: description.value,
+ fileIds: files.value.map(file => file.id),
+ isSensitive: isSensitive.value,
});
router.push(`/gallery/${created.id}`);
}
@@ -106,19 +106,19 @@ async function del() {
}
watch(() => props.postId, () => {
- init = () => props.postId ? os.api('gallery/posts/show', {
+ init.value = () => props.postId ? os.api('gallery/posts/show', {
postId: props.postId,
}).then(post => {
- files = post.files;
- title = post.title;
- description = post.description;
- isSensitive = post.isSensitive;
+ files.value = post.files;
+ title.value = post.title;
+ description.value = post.description;
+ isSensitive.value = post.isSensitive;
}) : Promise.resolve(null);
}, { immediate: true });
-const headerActions = $computed(() => []);
+const headerActions = computed(() => []);
-const headerTabs = $computed(() => []);
+const headerTabs = computed(() => []);
definePageMetadata(computed(() => props.postId ? {
title: i18n.ts.edit,
diff --git a/packages/frontend/src/pages/gallery/index.vue b/packages/frontend/src/pages/gallery/index.vue
index f65380eee1..936d9b8393 100644
--- a/packages/frontend/src/pages/gallery/index.vue
+++ b/packages/frontend/src/pages/gallery/index.vue
@@ -47,7 +47,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { watch } from 'vue';
+import { watch, ref, computed } from 'vue';
import MkFoldableSection from '@/components/MkFoldableSection.vue';
import MkPagination from '@/components/MkPagination.vue';
import MkGalleryPostPreview from '@/components/MkGalleryPostPreview.vue';
@@ -61,9 +61,9 @@ const props = defineProps<{
tag?: string;
}>();
-let tab = $ref('explore');
-let tags = $ref([]);
-let tagsRef = $ref();
+const tab = ref('explore');
+const tags = ref([]);
+const tagsRef = ref();
const recentPostsPagination = {
endpoint: 'gallery/posts' as const,
@@ -82,7 +82,7 @@ const likedPostsPagination = {
limit: 5,
};
-const tagUsersPagination = $computed(() => ({
+const tagUsersPagination = computed(() => ({
endpoint: 'hashtags/users' as const,
limit: 30,
params: {
@@ -93,10 +93,10 @@ const tagUsersPagination = $computed(() => ({
}));
watch(() => props.tag, () => {
- if (tagsRef) tagsRef.tags.toggleContent(props.tag == null);
+ if (tagsRef.value) tagsRef.value.tags.toggleContent(props.tag == null);
});
-const headerActions = $computed(() => [{
+const headerActions = computed(() => [{
icon: 'ph-plus ph-bold ph-lg',
text: i18n.ts.create,
handler: () => {
@@ -104,7 +104,7 @@ const headerActions = $computed(() => [{
},
}]);
-const headerTabs = $computed(() => [{
+const headerTabs = computed(() => [{
key: 'explore',
title: i18n.ts.gallery,
icon: 'ph-images-square ph-bold ph-lg',
diff --git a/packages/frontend/src/pages/gallery/post.vue b/packages/frontend/src/pages/gallery/post.vue
index ae609c994c..f9fa691580 100644
--- a/packages/frontend/src/pages/gallery/post.vue
+++ b/packages/frontend/src/pages/gallery/post.vue
@@ -29,7 +29,8 @@ SPDX-License-Identifier: AGPL-3.0-only
<div class="other">
<button v-if="$i && $i.id === post.user.id" v-tooltip="i18n.ts.edit" v-click-anime class="_button" @click="edit"><i class="ph-pencil ph-bold ph-lg ti-fw"></i></button>
<button v-tooltip="i18n.ts.shareWithNote" v-click-anime class="_button" @click="shareWithNote"><i class="ph-repeat ph-bold ph-lg ti-fw"></i></button>
- <button v-tooltip="i18n.ts.share" v-click-anime class="_button" @click="share"><i class="ph-share-network ph-bold ph-lg ti-fw"></i></button>
+ <button v-tooltip="i18n.ts.copyLink" v-click-anime class="_button" @click="copyLink"><i class="ph-share-network ph-bold ph-lg ti-fw"></i></button>
+ <button v-if="isSupportShare()" v-tooltip="i18n.ts.share" v-click-anime class="_button" @click="share"><i class="ph-share-network ph-bold ph-lg ti-fw"></i></button>
</div>
</div>
<div class="user">
@@ -61,7 +62,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { computed, watch } from 'vue';
+import { computed, watch, ref } from 'vue';
import MkButton from '@/components/MkButton.vue';
import * as os from '@/os.js';
import MkContainer from '@/components/MkContainer.vue';
@@ -74,6 +75,8 @@ import { i18n } from '@/i18n.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';
import { defaultStore } from '@/store.js';
import { $i } from '@/account.js';
+import { isSupportShare } from '@/scripts/navigator.js';
+import copyToClipboard from '@/scripts/copy-to-clipboard.js';
const router = useRouter();
@@ -81,38 +84,43 @@ const props = defineProps<{
postId: string;
}>();
-let post = $ref(null);
-let error = $ref(null);
+const post = ref(null);
+const error = ref(null);
const otherPostsPagination = {
endpoint: 'users/gallery/posts' as const,
limit: 6,
params: computed(() => ({
- userId: post.user.id,
+ userId: post.value.user.id,
})),
};
function fetchPost() {
- post = null;
+ post.value = null;
os.api('gallery/posts/show', {
postId: props.postId,
}).then(_post => {
- post = _post;
+ post.value = _post;
}).catch(_error => {
- error = _error;
+ error.value = _error;
});
}
+function copyLink() {
+ copyToClipboard(`${url}/gallery/${post.value.id}`);
+ os.success();
+}
+
function share() {
navigator.share({
- title: post.title,
- text: post.description,
- url: `${url}/gallery/${post.id}`,
+ title: post.value.title,
+ text: post.value.description,
+ url: `${url}/gallery/${post.value.id}`,
});
}
function shareWithNote() {
os.post({
- initialText: `${post.title} ${url}/gallery/${post.id}`,
+ initialText: `${post.value.title} ${url}/gallery/${post.value.id}`,
});
}
@@ -120,8 +128,8 @@ function like() {
os.apiWithDialog('gallery/posts/like', {
postId: props.postId,
}).then(() => {
- post.isLiked = true;
- post.likedCount++;
+ post.value.isLiked = true;
+ post.value.likedCount++;
});
}
@@ -134,28 +142,28 @@ async function unlike() {
os.apiWithDialog('gallery/posts/unlike', {
postId: props.postId,
}).then(() => {
- post.isLiked = false;
- post.likedCount--;
+ post.value.isLiked = false;
+ post.value.likedCount--;
});
}
function edit() {
- router.push(`/gallery/${post.id}/edit`);
+ router.push(`/gallery/${post.value.id}/edit`);
}
watch(() => props.postId, fetchPost, { immediate: true });
-const headerActions = $computed(() => [{
+const headerActions = computed(() => [{
icon: 'ph-pencil ph-bold ph-lg',
text: i18n.ts.edit,
handler: edit,
}]);
-const headerTabs = $computed(() => []);
+const headerTabs = computed(() => []);
-definePageMetadata(computed(() => post ? {
- title: post.title,
- avatar: post.user,
+definePageMetadata(computed(() => post.value ? {
+ title: post.value.title,
+ avatar: post.value.user,
} : null));
</script>
diff --git a/packages/frontend/src/pages/instance-info.vue b/packages/frontend/src/pages/instance-info.vue
index 668e4e61bf..3e1bc31df7 100644
--- a/packages/frontend/src/pages/instance-info.vue
+++ b/packages/frontend/src/pages/instance-info.vue
@@ -118,7 +118,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { } from 'vue';
+import { ref, computed } from 'vue';
import * as Misskey from 'misskey-js';
import MkChart from '@/components/MkChart.vue';
import MkObjectView from '@/components/MkObjectView.vue';
@@ -143,15 +143,15 @@ const props = defineProps<{
host: string;
}>();
-let tab = $ref('overview');
-let chartSrc = $ref('instance-requests');
-let meta = $ref<Misskey.entities.AdminInstanceMetadata | null>(null);
-let instance = $ref<Misskey.entities.Instance | null>(null);
-let suspended = $ref(false);
-let isBlocked = $ref(false);
-let isSilenced = $ref(false);
-let isNSFW = $ref(false);
-let faviconUrl = $ref<string | null>(null);
+const tab = ref('overview');
+const chartSrc = ref('instance-requests');
+const meta = ref<Misskey.entities.AdminMetaResponse | null>(null);
+const instance = ref<Misskey.entities.FederationInstance | null>(null);
+const suspended = ref(false);
+const isBlocked = ref(false);
+const isSilenced = ref(false);
+const isNSFW = ref(false);
+const faviconUrl = ref<string | null>(null);
const usersPagination = {
endpoint: iAmModerator ? 'admin/show-users' : 'users' as const,
@@ -166,41 +166,42 @@ const usersPagination = {
async function fetch(): Promise<void> {
if (iAmAdmin) {
- meta = await os.api('admin/meta');
+ meta.value = await os.api('admin/meta');
}
- instance = await os.api('federation/show-instance', {
+ instance.value = await os.api('federation/show-instance', {
host: props.host,
});
- suspended = instance.isSuspended;
- isBlocked = instance.isBlocked;
- isSilenced = instance.isSilenced;
- isNSFW = instance.isNSFW;
- faviconUrl = getProxiedImageUrlNullable(instance.faviconUrl, 'preview') ?? getProxiedImageUrlNullable(instance.iconUrl, 'preview');
+ suspended.value = instance.value?.isSuspended ?? false;
+ isBlocked.value = instance.value?.isBlocked ?? false;
+ isSilenced.value = instance.value?.isSilenced ?? false;
+ isNSFW.value = instance.value?.isNSFW ?? false;
+ faviconUrl.value = getProxiedImageUrlNullable(instance.value?.faviconUrl, 'preview') ?? getProxiedImageUrlNullable(instance.value?.iconUrl, 'preview');
}
async function toggleBlock(): Promise<void> {
- if (!meta) throw new Error('No meta?');
- if (!instance) throw new Error('No instance?');
- const { host } = instance;
+ if (!meta.value) throw new Error('No meta?');
+ if (!instance.value) throw new Error('No instance?');
+ const { host } = instance.value;
await os.api('admin/update-meta', {
- blockedHosts: isBlocked ? meta.blockedHosts.concat([host]) : meta.blockedHosts.filter(x => x !== host),
+ blockedHosts: isBlocked.value ? meta.value.blockedHosts.concat([host]) : meta.value.blockedHosts.filter(x => x !== host),
});
}
async function toggleSilenced(): Promise<void> {
- if (!meta) throw new Error('No meta?');
- if (!instance) throw new Error('No instance?');
- const { host } = instance;
+ if (!meta.value) throw new Error('No meta?');
+ if (!instance.value) throw new Error('No instance?');
+ const { host } = instance.value;
+ const silencedHosts = meta.value.silencedHosts ?? [];
await os.api('admin/update-meta', {
- silencedHosts: isSilenced ? meta.silencedHosts.concat([host]) : meta.silencedHosts.filter(x => x !== host),
+ silencedHosts: isSilenced.value ? silencedHosts.concat([host]) : silencedHosts.filter(x => x !== host),
});
}
async function toggleSuspend(): Promise<void> {
- if (!instance) throw new Error('No instance?');
+ if (!instance.value) throw new Error('No instance?');
await os.api('admin/federation/update-instance', {
- host: instance.host,
- isSuspended: suspended,
+ host: instance.value.host,
+ isSuspended: suspended.value,
});
}
@@ -213,9 +214,9 @@ async function toggleNSFW(): Promise<void> {
}
function refreshMetadata(): void {
- if (!instance) throw new Error('No instance?');
+ if (!instance.value) throw new Error('No instance?');
os.api('admin/federation/refresh-remote-instance-metadata', {
- host: instance.host,
+ host: instance.value.host,
});
os.alert({
text: 'Refresh requested',
@@ -224,15 +225,15 @@ function refreshMetadata(): void {
fetch();
-const headerActions = $computed(() => [{
+const headerActions = computed(() => [{
text: `https://${props.host}`,
icon: 'ph-arrow-square-out ph-bold ph-lg',
handler: () => {
- window.open(`https://${props.host}`, '_blank');
+ window.open(`https://${props.host}`, '_blank', 'noopener');
},
}]);
-const headerTabs = $computed(() => [{
+const headerTabs = computed(() => [{
key: 'overview',
title: i18n.ts.overview,
icon: 'ph-info ph-bold ph-lg',
diff --git a/packages/frontend/src/pages/invite.vue b/packages/frontend/src/pages/invite.vue
index a5bdc29dbb..6ac78a2068 100644
--- a/packages/frontend/src/pages/invite.vue
+++ b/packages/frontend/src/pages/invite.vue
@@ -26,7 +26,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkPagination ref="pagingComponent" :pagination="pagination">
<template #default="{ items }">
<div class="_gaps_s">
- <MkInviteCode v-for="item in (items as Misskey.entities.Invite[])" :key="item.id" :invite="item" :onDeleted="deleted"/>
+ <MkInviteCode v-for="item in (items as Misskey.entities.InviteCode[])" :key="item.id" :invite="item" :onDeleted="deleted"/>
</div>
</template>
</MkPagination>
diff --git a/packages/frontend/src/pages/list.vue b/packages/frontend/src/pages/list.vue
index 1323c6b3b9..731ad9f2ae 100644
--- a/packages/frontend/src/pages/list.vue
+++ b/packages/frontend/src/pages/list.vue
@@ -34,7 +34,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { watch, computed } from 'vue';
+import { watch, computed, ref } from 'vue';
import * as os from '@/os.js';
import { userPage } from '@/filters/user.js';
import { i18n } from '@/i18n.js';
@@ -47,41 +47,41 @@ const props = defineProps<{
listId: string;
}>();
-let list = $ref(null);
-let error = $ref();
-let users = $ref([]);
+const list = ref(null);
+const error = ref();
+const users = ref([]);
function fetchList(): void {
os.api('users/lists/show', {
listId: props.listId,
forPublic: true,
}).then(_list => {
- list = _list;
+ list.value = _list;
os.api('users/show', {
- userIds: list.userIds,
+ userIds: list.value.userIds,
}).then(_users => {
- users = _users;
+ users.value = _users;
});
}).catch(err => {
- error = err;
+ error.value = err;
});
}
function like() {
os.apiWithDialog('users/lists/favorite', {
- listId: list.id,
+ listId: list.value.id,
}).then(() => {
- list.isLiked = true;
- list.likedCount++;
+ list.value.isLiked = true;
+ list.value.likedCount++;
});
}
function unlike() {
os.apiWithDialog('users/lists/unfavorite', {
- listId: list.id,
+ listId: list.value.id,
}).then(() => {
- list.isLiked = false;
- list.likedCount--;
+ list.value.isLiked = false;
+ list.value.likedCount--;
});
}
@@ -90,17 +90,17 @@ async function create() {
title: i18n.ts.enterListName,
});
if (canceled) return;
- await os.apiWithDialog('users/lists/create-from-public', { name: name, listId: list.id });
+ await os.apiWithDialog('users/lists/create-from-public', { name: name, listId: list.value.id });
}
watch(() => props.listId, fetchList, { immediate: true });
-const headerActions = $computed(() => []);
+const headerActions = computed(() => []);
-const headerTabs = $computed(() => []);
+const headerTabs = computed(() => []);
-definePageMetadata(computed(() => list ? {
- title: list.name,
+definePageMetadata(computed(() => list.value ? {
+ title: list.value.name,
icon: 'ph-list ph-bold ph-lg',
} : null));
</script>
diff --git a/packages/frontend/src/pages/miauth.vue b/packages/frontend/src/pages/miauth.vue
index 1e3c627023..2b53b67ab3 100644
--- a/packages/frontend/src/pages/miauth.vue
+++ b/packages/frontend/src/pages/miauth.vue
@@ -43,7 +43,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { } from 'vue';
+import { ref, computed } from 'vue';
import MkSignin from '@/components/MkSignin.vue';
import MkButton from '@/components/MkButton.vue';
import * as os from '@/os.js';
@@ -59,20 +59,20 @@ const props = defineProps<{
permission: string; // コンマ区切り
}>();
-const _permissions = $computed(() => props.permission ? props.permission.split(',') : []);
+const _permissions = computed(() => props.permission ? props.permission.split(',') : []);
-let state = $ref<string | null>(null);
+const state = ref<string | null>(null);
async function accept(): Promise<void> {
- state = 'waiting';
+ state.value = 'waiting';
await os.api('miauth/gen-token', {
session: props.session,
name: props.name,
iconUrl: props.icon,
- permission: _permissions,
+ permission: _permissions.value,
});
- state = 'accepted';
+ state.value = 'accepted';
if (props.callback) {
const cbUrl = new URL(props.callback);
if (['javascript:', 'file:', 'data:', 'mailto:', 'tel:'].includes(cbUrl.protocol)) throw new Error('invalid url');
@@ -82,16 +82,16 @@ async function accept(): Promise<void> {
}
function deny(): void {
- state = 'denied';
+ state.value = 'denied';
}
function onLogin(res): void {
login(res.i);
}
-const headerActions = $computed(() => []);
+const headerActions = computed(() => []);
-const headerTabs = $computed(() => []);
+const headerTabs = computed(() => []);
definePageMetadata({
title: 'MiAuth',
diff --git a/packages/frontend/src/pages/my-antennas/create.vue b/packages/frontend/src/pages/my-antennas/create.vue
index d8613a0ba2..79b592dada 100644
--- a/packages/frontend/src/pages/my-antennas/create.vue
+++ b/packages/frontend/src/pages/my-antennas/create.vue
@@ -10,6 +10,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
+import { ref } from 'vue';
import XAntenna from './editor.vue';
import { i18n } from '@/i18n.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';
@@ -18,7 +19,7 @@ import { antennasCache } from '@/cache.js';
const router = useRouter();
-let draft = $ref({
+const draft = ref({
name: '',
src: 'all',
userListId: null,
diff --git a/packages/frontend/src/pages/my-antennas/edit.vue b/packages/frontend/src/pages/my-antennas/edit.vue
index 86410121d3..a325eb9b0f 100644
--- a/packages/frontend/src/pages/my-antennas/edit.vue
+++ b/packages/frontend/src/pages/my-antennas/edit.vue
@@ -10,6 +10,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
+import { ref } from 'vue';
import XAntenna from './editor.vue';
import * as os from '@/os.js';
import { i18n } from '@/i18n.js';
@@ -19,7 +20,7 @@ import { antennasCache } from '@/cache';
const router = useRouter();
-let antenna: any = $ref(null);
+const antenna = ref<any>(null);
const props = defineProps<{
antennaId: string
@@ -31,7 +32,7 @@ function onAntennaUpdated() {
}
os.api('antennas/show', { antennaId: props.antennaId }).then((antennaResponse) => {
- antenna = antennaResponse;
+ antenna.value = antennaResponse;
});
definePageMetadata({
diff --git a/packages/frontend/src/pages/my-antennas/editor.vue b/packages/frontend/src/pages/my-antennas/editor.vue
index ad60ba0a3c..5eacf15efb 100644
--- a/packages/frontend/src/pages/my-antennas/editor.vue
+++ b/packages/frontend/src/pages/my-antennas/editor.vue
@@ -49,7 +49,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { watch } from 'vue';
+import { watch, ref } from 'vue';
import * as Misskey from 'misskey-js';
import MkButton from '@/components/MkButton.vue';
import MkInput from '@/components/MkInput.vue';
@@ -69,38 +69,38 @@ const emit = defineEmits<{
(ev: 'deleted'): void,
}>();
-let name: string = $ref(props.antenna.name);
-let src: string = $ref(props.antenna.src);
-let userListId: any = $ref(props.antenna.userListId);
-let users: string = $ref(props.antenna.users.join('\n'));
-let keywords: string = $ref(props.antenna.keywords.map(x => x.join(' ')).join('\n'));
-let excludeKeywords: string = $ref(props.antenna.excludeKeywords.map(x => x.join(' ')).join('\n'));
-let caseSensitive: boolean = $ref(props.antenna.caseSensitive);
-let localOnly: boolean = $ref(props.antenna.localOnly);
-let withReplies: boolean = $ref(props.antenna.withReplies);
-let withFile: boolean = $ref(props.antenna.withFile);
-let notify: boolean = $ref(props.antenna.notify);
-let userLists: any = $ref(null);
+const name = ref<string>(props.antenna.name);
+const src = ref<string>(props.antenna.src);
+const userListId = ref<any>(props.antenna.userListId);
+const users = ref<string>(props.antenna.users.join('\n'));
+const keywords = ref<string>(props.antenna.keywords.map(x => x.join(' ')).join('\n'));
+const excludeKeywords = ref<string>(props.antenna.excludeKeywords.map(x => x.join(' ')).join('\n'));
+const caseSensitive = ref<boolean>(props.antenna.caseSensitive);
+const localOnly = ref<boolean>(props.antenna.localOnly);
+const withReplies = ref<boolean>(props.antenna.withReplies);
+const withFile = ref<boolean>(props.antenna.withFile);
+const notify = ref<boolean>(props.antenna.notify);
+const userLists = ref<any>(null);
-watch(() => src, async () => {
- if (src === 'list' && userLists === null) {
- userLists = await os.api('users/lists/list');
+watch(() => src.value, async () => {
+ if (src.value === 'list' && userLists.value === null) {
+ userLists.value = await os.api('users/lists/list');
}
});
async function saveAntenna() {
const antennaData = {
- name,
- src,
- userListId,
- withReplies,
- withFile,
- notify,
- caseSensitive,
- localOnly,
- users: users.trim().split('\n').map(x => x.trim()),
- keywords: keywords.trim().split('\n').map(x => x.trim().split(' ')),
- excludeKeywords: excludeKeywords.trim().split('\n').map(x => x.trim().split(' ')),
+ name: name.value,
+ src: src.value,
+ userListId: userListId.value,
+ withReplies: withReplies.value,
+ withFile: withFile.value,
+ notify: notify.value,
+ caseSensitive: caseSensitive.value,
+ localOnly: localOnly.value,
+ users: users.value.trim().split('\n').map(x => x.trim()),
+ keywords: keywords.value.trim().split('\n').map(x => x.trim().split(' ')),
+ excludeKeywords: excludeKeywords.value.trim().split('\n').map(x => x.trim().split(' ')),
};
if (props.antenna.id == null) {
@@ -130,9 +130,9 @@ async function deleteAntenna() {
function addUser() {
os.selectUser().then(user => {
- users = users.trim();
- users += '\n@' + Misskey.acct.toString(user as any);
- users = users.trim();
+ users.value = users.value.trim();
+ users.value += '\n@' + Misskey.acct.toString(user as any);
+ users.value = users.value.trim();
});
}
</script>
diff --git a/packages/frontend/src/pages/my-antennas/index.vue b/packages/frontend/src/pages/my-antennas/index.vue
index 5c6432b80d..2a5fe07957 100644
--- a/packages/frontend/src/pages/my-antennas/index.vue
+++ b/packages/frontend/src/pages/my-antennas/index.vue
@@ -28,14 +28,14 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { onActivated } from 'vue';
+import { onActivated, computed } from 'vue';
import MkButton from '@/components/MkButton.vue';
import { i18n } from '@/i18n.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';
import { antennasCache } from '@/cache';
import { infoImageUrl } from '@/instance.js';
-const antennas = $computed(() => antennasCache.value.value ?? []);
+const antennas = computed(() => antennasCache.value.value ?? []);
function fetch() {
antennasCache.fetch();
@@ -43,7 +43,7 @@ function fetch() {
fetch();
-const headerActions = $computed(() => [{
+const headerActions = computed(() => [{
asFullButton: true,
icon: 'ph-arrows-counter-clockwise ph-bold ph-lg',
text: i18n.ts.reload,
@@ -53,7 +53,7 @@ const headerActions = $computed(() => [{
},
}]);
-const headerTabs = $computed(() => []);
+const headerTabs = computed(() => []);
definePageMetadata({
title: i18n.ts.manageAntennas,
diff --git a/packages/frontend/src/pages/my-clips/index.vue b/packages/frontend/src/pages/my-clips/index.vue
index eb84112c69..31ccf9ce2d 100644
--- a/packages/frontend/src/pages/my-clips/index.vue
+++ b/packages/frontend/src/pages/my-clips/index.vue
@@ -26,7 +26,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { watch } from 'vue';
+import { watch, ref, shallowRef, computed } from 'vue';
import MkPagination from '@/components/MkPagination.vue';
import MkButton from '@/components/MkButton.vue';
import MkClipPreview from '@/components/MkClipPreview.vue';
@@ -41,13 +41,13 @@ const pagination = {
limit: 10,
};
-let tab = $ref('my');
-let favorites = $ref();
+const tab = ref('my');
+const favorites = ref();
-const pagingComponent = $shallowRef<InstanceType<typeof MkPagination>>();
+const pagingComponent = shallowRef<InstanceType<typeof MkPagination>>();
-watch($$(tab), async () => {
- favorites = await os.api('clips/my-favorites');
+watch(tab, async () => {
+ favorites.value = await os.api('clips/my-favorites');
});
async function create() {
@@ -60,6 +60,7 @@ async function create() {
type: 'string',
required: false,
multiline: true,
+ treatAsMfm: true,
label: i18n.ts.description,
},
isPublic: {
@@ -74,20 +75,20 @@ async function create() {
clipsCache.delete();
- pagingComponent.reload();
+ pagingComponent.value.reload();
}
function onClipCreated() {
- pagingComponent.reload();
+ pagingComponent.value.reload();
}
function onClipDeleted() {
- pagingComponent.reload();
+ pagingComponent.value.reload();
}
-const headerActions = $computed(() => []);
+const headerActions = computed(() => []);
-const headerTabs = $computed(() => [{
+const headerTabs = computed(() => [{
key: 'my',
title: i18n.ts.myClips,
icon: 'ph-paperclip ph-bold ph-lg',
diff --git a/packages/frontend/src/pages/my-lists/index.vue b/packages/frontend/src/pages/my-lists/index.vue
index 147eaa12c3..b0d750b218 100644
--- a/packages/frontend/src/pages/my-lists/index.vue
+++ b/packages/frontend/src/pages/my-lists/index.vue
@@ -29,7 +29,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { onActivated } from 'vue';
+import { onActivated, computed } from 'vue';
import MkButton from '@/components/MkButton.vue';
import MkAvatars from '@/components/MkAvatars.vue';
import * as os from '@/os.js';
@@ -39,7 +39,7 @@ import { userListsCache } from '@/cache';
import { infoImageUrl } from '@/instance.js';
import { $i } from '@/account.js';
-const items = $computed(() => userListsCache.value.value ?? []);
+const items = computed(() => userListsCache.value.value ?? []);
function fetch() {
userListsCache.fetch();
@@ -57,7 +57,7 @@ async function create() {
fetch();
}
-const headerActions = $computed(() => [{
+const headerActions = computed(() => [{
asFullButton: true,
icon: 'ph-arrows-counter-clockwise ph-bold ph-lg',
text: i18n.ts.reload,
@@ -67,7 +67,7 @@ const headerActions = $computed(() => [{
},
}]);
-const headerTabs = $computed(() => []);
+const headerTabs = computed(() => []);
definePageMetadata({
title: i18n.ts.manageLists,
diff --git a/packages/frontend/src/pages/my-lists/list.vue b/packages/frontend/src/pages/my-lists/list.vue
index 5c6e418264..df9cdb0fce 100644
--- a/packages/frontend/src/pages/my-lists/list.vue
+++ b/packages/frontend/src/pages/my-lists/list.vue
@@ -68,7 +68,7 @@ import MkInput from '@/components/MkInput.vue';
import { userListsCache } from '@/cache.js';
import { $i } from '@/account.js';
import { defaultStore } from '@/store.js';
-import MkPagination, { Paging } from '@/components/MkPagination.vue';
+import MkPagination from '@/components/MkPagination.vue';
const {
enableInfiniteScroll,
@@ -79,7 +79,7 @@ const props = defineProps<{
}>();
const paginationEl = ref<InstanceType<typeof MkPagination>>();
-let list = $ref<Misskey.entities.UserList | null>(null);
+const list = ref<Misskey.entities.UserList | null>(null);
const isPublic = ref(false);
const name = ref('');
const membershipsPagination = {
@@ -94,17 +94,17 @@ function fetchList() {
os.api('users/lists/show', {
listId: props.listId,
}).then(_list => {
- list = _list;
- name.value = list.name;
- isPublic.value = list.isPublic;
+ list.value = _list;
+ name.value = list.value.name;
+ isPublic.value = list.value.isPublic;
});
}
function addUser() {
os.selectUser().then(user => {
- if (!list) return;
+ if (!list.value) return;
os.apiWithDialog('users/lists/push', {
- listId: list.id,
+ listId: list.value.id,
userId: user.id,
}).then(() => {
paginationEl.value.reload();
@@ -118,9 +118,9 @@ async function removeUser(item, ev) {
icon: 'ph-x ph-bold ph-lg',
danger: true,
action: async () => {
- if (!list) return;
+ if (!list.value) return;
os.api('users/lists/pull', {
- listId: list.id,
+ listId: list.value.id,
userId: item.userId,
}).then(() => {
paginationEl.value.removeItem(item.id);
@@ -135,7 +135,7 @@ async function showMembershipMenu(item, ev) {
icon: item.withReplies ? 'ph-envelope-open ph-bold ph-lg' : 'ph-envelope ph-bold ph-lg',
action: async () => {
os.api('users/lists/update-membership', {
- listId: list.id,
+ listId: list.value.id,
userId: item.userId,
withReplies: !item.withReplies,
}).then(() => {
@@ -149,42 +149,42 @@ async function showMembershipMenu(item, ev) {
}
async function deleteList() {
- if (!list) return;
+ if (!list.value) return;
const { canceled } = await os.confirm({
type: 'warning',
- text: i18n.t('removeAreYouSure', { x: list.name }),
+ text: i18n.t('removeAreYouSure', { x: list.value.name }),
});
if (canceled) return;
await os.apiWithDialog('users/lists/delete', {
- listId: list.id,
+ listId: list.value.id,
});
userListsCache.delete();
mainRouter.push('/my/lists');
}
async function updateSettings() {
- if (!list) return;
+ if (!list.value) return;
await os.apiWithDialog('users/lists/update', {
- listId: list.id,
+ listId: list.value.id,
name: name.value,
isPublic: isPublic.value,
});
userListsCache.delete();
- list.name = name.value;
- list.isPublic = isPublic.value;
+ list.value.name = name.value;
+ list.value.isPublic = isPublic.value;
}
watch(() => props.listId, fetchList, { immediate: true });
-const headerActions = $computed(() => []);
+const headerActions = computed(() => []);
-const headerTabs = $computed(() => []);
+const headerTabs = computed(() => []);
-definePageMetadata(computed(() => list ? {
- title: list.name,
+definePageMetadata(computed(() => list.value ? {
+ title: list.value.name,
icon: 'ph-list ph-bold ph-lg',
} : null));
</script>
diff --git a/packages/frontend/src/pages/not-found.vue b/packages/frontend/src/pages/not-found.vue
index 410979bdc6..e8ba31395e 100644
--- a/packages/frontend/src/pages/not-found.vue
+++ b/packages/frontend/src/pages/not-found.vue
@@ -13,6 +13,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
+import { computed } from 'vue';
import { i18n } from '@/i18n.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';
import { pleaseLogin } from '@/scripts/please-login.js';
@@ -26,9 +27,9 @@ if (props.showLoginPopup) {
pleaseLogin('/');
}
-const headerActions = $computed(() => []);
+const headerActions = computed(() => []);
-const headerTabs = $computed(() => []);
+const headerTabs = computed(() => []);
definePageMetadata({
title: i18n.ts.notFound,
diff --git a/packages/frontend/src/pages/note.vue b/packages/frontend/src/pages/note.vue
index 14f9ff3816..1e62ca9f61 100644
--- a/packages/frontend/src/pages/note.vue
+++ b/packages/frontend/src/pages/note.vue
@@ -48,7 +48,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { computed, watch } from 'vue';
+import { computed, watch, ref } from 'vue';
import * as Misskey from 'misskey-js';
import MkNoteDetailed from '@/components/MkNoteDetailed.vue';
import MkNotes from '@/components/MkNotes.vue';
@@ -66,19 +66,19 @@ const props = defineProps<{
noteId: string;
}>();
-let note = $ref<null | Misskey.entities.Note>();
-let clips = $ref();
-let showPrev = $ref(false);
-let showNext = $ref(false);
-let expandAllCws = $ref(false);
-let error = $ref();
+const note = ref<null | Misskey.entities.Note>();
+const clips = ref();
+const showPrev = ref(false);
+const showNext = ref(false);
+const expandAllCws = ref(false);
+const error = ref();
const prevPagination = {
endpoint: 'users/notes' as const,
limit: 10,
- params: computed(() => note ? ({
- userId: note.userId,
- untilId: note.id,
+ params: computed(() => note.value ? ({
+ userId: note.value.userId,
+ untilId: note.value.id,
}) : null),
};
@@ -86,30 +86,30 @@ const nextPagination = {
reversed: true,
endpoint: 'users/notes' as const,
limit: 10,
- params: computed(() => note ? ({
- userId: note.userId,
- sinceId: note.id,
+ params: computed(() => note.value ? ({
+ userId: note.value.userId,
+ sinceId: note.value.id,
}) : null),
};
function fetchNote() {
- showPrev = false;
- showNext = false;
- note = null;
+ showPrev.value = false;
+ showNext.value = false;
+ note.value = null;
os.api('notes/show', {
noteId: props.noteId,
}).then(res => {
- note = res;
+ note.value = res;
// 古いノートは被クリップ数をカウントしていないので、2023-10-01以前のものは強制的にnotes/clipsを叩く
- if (note.clippedCount > 0 || new Date(note.createdAt).getTime() < new Date('2023-10-01').getTime()) {
+ if (note.value.clippedCount > 0 || new Date(note.value.createdAt).getTime() < new Date('2023-10-01').getTime()) {
os.api('notes/clips', {
- noteId: note.id,
+ noteId: note.value.id,
}).then((_clips) => {
- clips = _clips;
+ clips.value = _clips;
});
}
}).catch(err => {
- error = err;
+ error.value = err;
});
}
@@ -117,24 +117,24 @@ watch(() => props.noteId, fetchNote, {
immediate: true,
});
-const headerActions = $computed(() => note ? [
+const headerActions = computed(() => note.value ? [
{
- icon: `${expandAllCws ? 'ph-eye' : 'ph-eye-slash'} ph-bold ph-lg`,
- text: expandAllCws ? i18n.ts.collapseAllCws : i18n.ts.expandAllCws,
- handler: () => { expandAllCws = !expandAllCws; },
+ icon: `${expandAllCws.value ? 'ph-eye' : 'ph-eye-slash'} ph-bold ph-lg`,
+ text: expandAllCws.value ? i18n.ts.collapseAllCws : i18n.ts.expandAllCws,
+ handler: () => { expandAllCws.value = !expandAllCws.value; },
},
] : []);
-const headerTabs = $computed(() => []);
+const headerTabs = computed(() => []);
-definePageMetadata(computed(() => note ? {
+definePageMetadata(computed(() => note.value ? {
title: i18n.ts.note,
- subtitle: dateString(note.createdAt),
- avatar: note.user,
- path: `/notes/${note.id}`,
+ subtitle: dateString(note.value.createdAt),
+ avatar: note.value.user,
+ path: `/notes/${note.value.id}`,
share: {
- title: i18n.t('noteOf', { user: note.user.name }),
- text: note.text,
+ title: i18n.t('noteOf', { user: note.value.user.name }),
+ text: note.value.text,
},
} : null));
</script>
diff --git a/packages/frontend/src/pages/notifications.vue b/packages/frontend/src/pages/notifications.vue
index baccb4916c..f3fadf5c8e 100644
--- a/packages/frontend/src/pages/notifications.vue
+++ b/packages/frontend/src/pages/notifications.vue
@@ -21,7 +21,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { computed } from 'vue';
+import { computed, ref } from 'vue';
import XNotifications from '@/components/MkNotifications.vue';
import MkNotes from '@/components/MkNotes.vue';
import * as os from '@/os.js';
@@ -29,9 +29,9 @@ import { i18n } from '@/i18n.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';
import { notificationTypes } from '@/const.js';
-let tab = $ref('all');
-let includeTypes = $ref<string[] | null>(null);
-const excludeTypes = $computed(() => includeTypes ? notificationTypes.filter(t => !includeTypes.includes(t)) : null);
+const tab = ref('all');
+const includeTypes = ref<string[] | null>(null);
+const excludeTypes = computed(() => includeTypes.value ? notificationTypes.filter(t => !includeTypes.value.includes(t)) : null);
const mentionsPagination = {
endpoint: 'notes/mentions' as const,
@@ -49,27 +49,27 @@ const directNotesPagination = {
function setFilter(ev) {
const typeItems = notificationTypes.map(t => ({
text: i18n.t(`_notification._types.${t}`),
- active: includeTypes && includeTypes.includes(t),
+ active: includeTypes.value && includeTypes.value.includes(t),
action: () => {
- includeTypes = [t];
+ includeTypes.value = [t];
},
}));
- const items = includeTypes != null ? [{
+ const items = includeTypes.value != null ? [{
icon: 'ph-x ph-bold ph-lg',
text: i18n.ts.clear,
action: () => {
- includeTypes = null;
+ includeTypes.value = null;
},
- }, null, ...typeItems] : typeItems;
+ }, { type: 'divider' }, ...typeItems] : typeItems;
os.popupMenu(items, ev.currentTarget ?? ev.target);
}
-const headerActions = $computed(() => [tab === 'all' ? {
+const headerActions = computed(() => [tab.value === 'all' ? {
text: i18n.ts.filter,
icon: 'ph-funnel ph-bold ph-lg',
- highlighted: includeTypes != null,
+ highlighted: includeTypes.value != null,
handler: setFilter,
-} : undefined, tab === 'all' ? {
+} : undefined, tab.value === 'all' ? {
text: i18n.ts.markAllAsRead,
icon: 'ph-check ph-bold ph-lg',
handler: () => {
@@ -77,7 +77,7 @@ const headerActions = $computed(() => [tab === 'all' ? {
},
} : undefined].filter(x => x !== undefined));
-const headerTabs = $computed(() => [{
+const headerTabs = computed(() => [{
key: 'all',
title: i18n.ts.all,
icon: 'ph-circle ph-bold ph-lg',
diff --git a/packages/frontend/src/pages/page-editor/els/page-editor.el.image.vue b/packages/frontend/src/pages/page-editor/els/page-editor.el.image.vue
index 2cb126c80f..f97c5ea1a7 100644
--- a/packages/frontend/src/pages/page-editor/els/page-editor.el.image.vue
+++ b/packages/frontend/src/pages/page-editor/els/page-editor.el.image.vue
@@ -21,7 +21,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
/* eslint-disable vue/no-mutating-props */
-import { onMounted } from 'vue';
+import { onMounted, ref } from 'vue';
import XContainer from '../page-editor.container.vue';
import MkDriveFileThumbnail from '@/components/MkDriveFileThumbnail.vue';
import * as os from '@/os.js';
@@ -35,14 +35,14 @@ const emit = defineEmits<{
(ev: 'update:modelValue', value: any): void;
}>();
-let file: any = $ref(null);
+const file = ref<any>(null);
async function choose() {
os.selectDriveFile(false).then((fileResponse) => {
- file = fileResponse[0];
+ file.value = fileResponse[0];
emit('update:modelValue', {
...props.modelValue,
- fileId: file.id,
+ fileId: file.value.id,
});
});
}
@@ -54,7 +54,7 @@ onMounted(async () => {
os.api('drive/files/show', {
fileId: props.modelValue.fileId,
}).then(fileResponse => {
- file = fileResponse;
+ file.value = fileResponse;
});
}
});
diff --git a/packages/frontend/src/pages/page-editor/els/page-editor.el.note.vue b/packages/frontend/src/pages/page-editor/els/page-editor.el.note.vue
index 0ed6c9bfa4..fc11ca8543 100644
--- a/packages/frontend/src/pages/page-editor/els/page-editor.el.note.vue
+++ b/packages/frontend/src/pages/page-editor/els/page-editor.el.note.vue
@@ -23,7 +23,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
/* eslint-disable vue/no-mutating-props */
-import { watch } from 'vue';
+import { watch, ref } from 'vue';
import XContainer from '../page-editor.container.vue';
import MkInput from '@/components/MkInput.vue';
import MkSwitch from '@/components/MkSwitch.vue';
@@ -40,19 +40,19 @@ const emit = defineEmits<{
(ev: 'update:modelValue', value: any): void;
}>();
-let id: any = $ref(props.modelValue.note);
-let note: any = $ref(null);
+const id = ref<any>(props.modelValue.note);
+const note = ref<any>(null);
-watch($$(id), async () => {
- if (id && (id.startsWith('http://') || id.startsWith('https://'))) {
- id = (id.endsWith('/') ? id.slice(0, -1) : id).split('/').pop();
+watch(id, async () => {
+ if (id.value && (id.value.startsWith('http://') || id.value.startsWith('https://'))) {
+ id.value = (id.value.endsWith('/') ? id.value.slice(0, -1) : id.value).split('/').pop();
}
emit('update:modelValue', {
...props.modelValue,
- note: id,
+ note: id.value,
});
- note = await os.api('notes/show', { noteId: id });
+ note.value = await os.api('notes/show', { noteId: id.value });
}, {
immediate: true,
});
diff --git a/packages/frontend/src/pages/page-editor/els/page-editor.el.section.vue b/packages/frontend/src/pages/page-editor/els/page-editor.el.section.vue
index f24131ebb7..885fa55bc9 100644
--- a/packages/frontend/src/pages/page-editor/els/page-editor.el.section.vue
+++ b/packages/frontend/src/pages/page-editor/els/page-editor.el.section.vue
@@ -22,7 +22,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
/* eslint-disable vue/no-mutating-props */
-import { defineAsyncComponent, inject, onMounted, watch } from 'vue';
+import { defineAsyncComponent, inject, onMounted, watch, ref } from 'vue';
import { v4 as uuid } from 'uuid';
import XContainer from '../page-editor.container.vue';
import * as os from '@/os.js';
@@ -42,12 +42,12 @@ const emit = defineEmits<{
(ev: 'update:modelValue', value: any): void;
}>();
-const children = $ref(deepClone(props.modelValue.children ?? []));
+const children = ref(deepClone(props.modelValue.children ?? []));
-watch($$(children), () => {
+watch(children, () => {
emit('update:modelValue', {
...props.modelValue,
- children,
+ children: children.value,
});
}, {
deep: true,
@@ -75,7 +75,7 @@ async function add() {
if (canceled) return;
const id = uuid();
- children.push({ id, type });
+ children.value.push({ id, type });
}
onMounted(() => {
diff --git a/packages/frontend/src/pages/page-editor/els/page-editor.el.text.vue b/packages/frontend/src/pages/page-editor/els/page-editor.el.text.vue
index 8df74374b2..2af4e4e365 100644
--- a/packages/frontend/src/pages/page-editor/els/page-editor.el.text.vue
+++ b/packages/frontend/src/pages/page-editor/els/page-editor.el.text.vue
@@ -9,16 +9,17 @@ SPDX-License-Identifier: AGPL-3.0-only
<template #header><i class="ph-text-align-left ph-bold ph-lg"></i> {{ i18n.ts._pages.blocks.text }}</template>
<section>
- <textarea v-model="text" :class="$style.textarea"></textarea>
+ <textarea ref="inputEl" v-model="text" :class="$style.textarea"></textarea>
</section>
</XContainer>
</template>
<script lang="ts" setup>
/* eslint-disable vue/no-mutating-props */
-import { watch } from 'vue';
+import { watch, ref, shallowRef, onMounted, onUnmounted } from 'vue';
import XContainer from '../page-editor.container.vue';
import { i18n } from '@/i18n.js';
+import { Autocomplete } from '@/scripts/autocomplete.js';
const props = defineProps<{
modelValue: any
@@ -28,14 +29,25 @@ const emit = defineEmits<{
(ev: 'update:modelValue', value: any): void;
}>();
-const text = $ref(props.modelValue.text ?? '');
+let autocomplete: Autocomplete;
-watch($$(text), () => {
+const text = ref(props.modelValue.text ?? '');
+const inputEl = shallowRef<HTMLTextAreaElement | null>(null);
+
+watch(text, () => {
emit('update:modelValue', {
...props.modelValue,
- text,
+ text: text.value,
});
});
+
+onMounted(() => {
+ autocomplete = new Autocomplete(inputEl.value, text);
+});
+
+onUnmounted(() => {
+ autocomplete.detach();
+});
</script>
<style lang="scss" module>
diff --git a/packages/frontend/src/pages/page-editor/page-editor.container.vue b/packages/frontend/src/pages/page-editor/page-editor.container.vue
index 10effdda1e..71fa890f63 100644
--- a/packages/frontend/src/pages/page-editor/page-editor.container.vue
+++ b/packages/frontend/src/pages/page-editor/page-editor.container.vue
@@ -29,7 +29,6 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { ref } from 'vue';
-import { i18n } from '@/i18n.js';
const props = withDefaults(defineProps<{
expanded?: boolean;
diff --git a/packages/frontend/src/pages/page-editor/page-editor.vue b/packages/frontend/src/pages/page-editor/page-editor.vue
index f455f8904e..a6e68686df 100644
--- a/packages/frontend/src/pages/page-editor/page-editor.vue
+++ b/packages/frontend/src/pages/page-editor/page-editor.vue
@@ -61,7 +61,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { computed, provide, watch } from 'vue';
+import { computed, provide, watch, ref } from 'vue';
import { v4 as uuid } from 'uuid';
import XBlocks from './page-editor.blocks.vue';
import MkButton from '@/components/MkButton.vue';
@@ -81,47 +81,47 @@ const props = defineProps<{
initUser?: string;
}>();
-let tab = $ref('settings');
-let author = $ref($i);
-let readonly = $ref(false);
-let page = $ref(null);
-let pageId = $ref(null);
-let currentName = $ref(null);
-let title = $ref('');
-let summary = $ref(null);
-let name = $ref(Date.now().toString());
-let eyeCatchingImage = $ref(null);
-let eyeCatchingImageId = $ref(null);
-let font = $ref('sans-serif');
-let content = $ref([]);
-let alignCenter = $ref(false);
-let hideTitleWhenPinned = $ref(false);
+const tab = ref('settings');
+const author = ref($i);
+const readonly = ref(false);
+const page = ref(null);
+const pageId = ref(null);
+const currentName = ref(null);
+const title = ref('');
+const summary = ref(null);
+const name = ref(Date.now().toString());
+const eyeCatchingImage = ref(null);
+const eyeCatchingImageId = ref(null);
+const font = ref('sans-serif');
+const content = ref([]);
+const alignCenter = ref(false);
+const hideTitleWhenPinned = ref(false);
-provide('readonly', readonly);
+provide('readonly', readonly.value);
provide('getPageBlockList', getPageBlockList);
-watch($$(eyeCatchingImageId), async () => {
- if (eyeCatchingImageId == null) {
- eyeCatchingImage = null;
+watch(eyeCatchingImageId, async () => {
+ if (eyeCatchingImageId.value == null) {
+ eyeCatchingImage.value = null;
} else {
- eyeCatchingImage = await os.api('drive/files/show', {
- fileId: eyeCatchingImageId,
+ eyeCatchingImage.value = await os.api('drive/files/show', {
+ fileId: eyeCatchingImageId.value,
});
}
});
function getSaveOptions() {
return {
- title: title.trim(),
- name: name.trim(),
- summary: summary,
- font: font,
+ title: title.value.trim(),
+ name: name.value.trim(),
+ summary: summary.value,
+ font: font.value,
script: '',
- hideTitleWhenPinned: hideTitleWhenPinned,
- alignCenter: alignCenter,
- content: content,
+ hideTitleWhenPinned: hideTitleWhenPinned.value,
+ alignCenter: alignCenter.value,
+ content: content.value,
variables: [],
- eyeCatchingImageId: eyeCatchingImageId,
+ eyeCatchingImageId: eyeCatchingImageId.value,
};
}
@@ -145,11 +145,11 @@ function save() {
}
};
- if (pageId) {
- options.pageId = pageId;
+ if (pageId.value) {
+ options.pageId = pageId.value;
os.api('pages/update', options)
.then(page => {
- currentName = name.trim();
+ currentName.value = name.value.trim();
os.alert({
type: 'success',
text: i18n.ts._pages.updated,
@@ -158,13 +158,13 @@ function save() {
} else {
os.api('pages/create', options)
.then(created => {
- pageId = created.id;
- currentName = name.trim();
+ pageId.value = created.id;
+ currentName.value = name.value.trim();
os.alert({
type: 'success',
text: i18n.ts._pages.created,
});
- mainRouter.push(`/pages/edit/${pageId}`);
+ mainRouter.push(`/pages/edit/${pageId.value}`);
}).catch(onError);
}
}
@@ -172,11 +172,11 @@ function save() {
function del() {
os.confirm({
type: 'warning',
- text: i18n.t('removeAreYouSure', { x: title.trim() }),
+ text: i18n.t('removeAreYouSure', { x: title.value.trim() }),
}).then(({ canceled }) => {
if (canceled) return;
os.api('pages/delete', {
- pageId: pageId,
+ pageId: pageId.value,
}).then(() => {
os.alert({
type: 'success',
@@ -188,16 +188,16 @@ function del() {
}
function duplicate() {
- title = title + ' - copy';
- name = name + '-copy';
+ title.value = title.value + ' - copy';
+ name.value = name.value + '-copy';
os.api('pages/create', getSaveOptions()).then(created => {
- pageId = created.id;
- currentName = name.trim();
+ pageId.value = created.id;
+ currentName.value = name.value.trim();
os.alert({
type: 'success',
text: i18n.ts._pages.created,
});
- mainRouter.push(`/pages/edit/${pageId}`);
+ mainRouter.push(`/pages/edit/${pageId.value}`);
});
}
@@ -210,7 +210,7 @@ async function add() {
if (canceled) return;
const id = uuid();
- content.push({ id, type });
+ content.value.push({ id, type });
}
function getPageBlockList() {
@@ -224,42 +224,42 @@ function getPageBlockList() {
function setEyeCatchingImage(img) {
selectFile(img.currentTarget ?? img.target, null).then(file => {
- eyeCatchingImageId = file.id;
+ eyeCatchingImageId.value = file.id;
});
}
function removeEyeCatchingImage() {
- eyeCatchingImageId = null;
+ eyeCatchingImageId.value = null;
}
async function init() {
if (props.initPageId) {
- page = await os.api('pages/show', {
+ page.value = await os.api('pages/show', {
pageId: props.initPageId,
});
} else if (props.initPageName && props.initUser) {
- page = await os.api('pages/show', {
+ page.value = await os.api('pages/show', {
name: props.initPageName,
username: props.initUser,
});
- readonly = true;
+ readonly.value = true;
}
- if (page) {
- author = page.user;
- pageId = page.id;
- title = page.title;
- name = page.name;
- currentName = page.name;
- summary = page.summary;
- font = page.font;
- hideTitleWhenPinned = page.hideTitleWhenPinned;
- alignCenter = page.alignCenter;
- content = page.content;
- eyeCatchingImageId = page.eyeCatchingImageId;
+ if (page.value) {
+ author.value = page.value.user;
+ pageId.value = page.value.id;
+ title.value = page.value.title;
+ name.value = page.value.name;
+ currentName.value = page.value.name;
+ summary.value = page.value.summary;
+ font.value = page.value.font;
+ hideTitleWhenPinned.value = page.value.hideTitleWhenPinned;
+ alignCenter.value = page.value.alignCenter;
+ content.value = page.value.content;
+ eyeCatchingImageId.value = page.value.eyeCatchingImageId;
} else {
const id = uuid();
- content = [{
+ content.value = [{
id,
type: 'text',
text: 'Hello World!',
@@ -269,9 +269,9 @@ async function init() {
init();
-const headerActions = $computed(() => []);
+const headerActions = computed(() => []);
-const headerTabs = $computed(() => [{
+const headerTabs = computed(() => [{
key: 'settings',
title: i18n.ts._pages.pageSetting,
icon: 'ph-gear ph-bold ph-lg',
diff --git a/packages/frontend/src/pages/page.vue b/packages/frontend/src/pages/page.vue
index 962d910405..2cfb37ffa7 100644
--- a/packages/frontend/src/pages/page.vue
+++ b/packages/frontend/src/pages/page.vue
@@ -34,7 +34,8 @@ SPDX-License-Identifier: AGPL-3.0-only
</div>
<div class="other">
<button v-tooltip="i18n.ts.shareWithNote" v-click-anime class="_button" @click="shareWithNote"><i class="ph-rocket-launch ph-bold ph-lg ti-fw"></i></button>
- <button v-tooltip="i18n.ts.share" v-click-anime class="_button" @click="share"><i class="ph-share-network ph-bold ph-lg ti-fw"></i></button>
+ <button v-tooltip="i18n.ts.copyLink" v-click-anime class="_button" @click="copyLink"><i class="ph-share-network ph-bold ph-lg ti-fw"></i></button>
+ <button v-if="isSupportShare()" v-tooltip="i18n.ts.share" v-click-anime class="_button" @click="share"><i class="ph-share-network ph-bold ph-lg ti-fw"></i></button>
</div>
</div>
<div class="user">
@@ -75,7 +76,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { computed, watch } from 'vue';
+import { computed, watch, ref } from 'vue';
import XPage from '@/components/page/page.vue';
import MkButton from '@/components/MkButton.vue';
import * as os from '@/os.js';
@@ -90,30 +91,32 @@ import { definePageMetadata } from '@/scripts/page-metadata.js';
import { pageViewInterruptors, defaultStore } from '@/store.js';
import { deepClone } from '@/scripts/clone.js';
import { $i } from '@/account.js';
+import { isSupportShare } from '@/scripts/navigator.js';
+import copyToClipboard from '@/scripts/copy-to-clipboard.js';
const props = defineProps<{
pageName: string;
username: string;
}>();
-let page = $ref(null);
-let error = $ref(null);
+const page = ref(null);
+const error = ref(null);
const otherPostsPagination = {
endpoint: 'users/pages' as const,
limit: 6,
params: computed(() => ({
- userId: page.user.id,
+ userId: page.value.user.id,
})),
};
-const path = $computed(() => props.username + '/' + props.pageName);
+const path = computed(() => props.username + '/' + props.pageName);
function fetchPage() {
- page = null;
+ page.value = null;
os.api('pages/show', {
name: props.pageName,
username: props.username,
}).then(async _page => {
- page = _page;
+ page.value = _page;
// plugin
if (pageViewInterruptors.length > 0) {
@@ -121,33 +124,38 @@ function fetchPage() {
for (const interruptor of pageViewInterruptors) {
result = await interruptor.handler(result);
}
- page = result;
+ page.value = result;
}
}).catch(err => {
- error = err;
+ error.value = err;
});
}
function share() {
navigator.share({
- title: page.title ?? page.name,
- text: page.summary,
- url: `${url}/@${page.user.username}/pages/${page.name}`,
+ title: page.value.title ?? page.value.name,
+ text: page.value.summary,
+ url: `${url}/@${page.value.user.username}/pages/${page.value.name}`,
});
}
+function copyLink() {
+ copyToClipboard(`${url}/@${page.value.user.username}/pages/${page.value.name}`);
+ os.success();
+}
+
function shareWithNote() {
os.post({
- initialText: `${page.title || page.name} ${url}/@${page.user.username}/pages/${page.name}`,
+ initialText: `${page.value.title || page.value.name} ${url}/@${page.value.user.username}/pages/${page.value.name}`,
});
}
function like() {
os.apiWithDialog('pages/like', {
- pageId: page.id,
+ pageId: page.value.id,
}).then(() => {
- page.isLiked = true;
- page.likedCount++;
+ page.value.isLiked = true;
+ page.value.likedCount++;
});
}
@@ -158,32 +166,32 @@ async function unlike() {
});
if (confirm.canceled) return;
os.apiWithDialog('pages/unlike', {
- pageId: page.id,
+ pageId: page.value.id,
}).then(() => {
- page.isLiked = false;
- page.likedCount--;
+ page.value.isLiked = false;
+ page.value.likedCount--;
});
}
function pin(pin) {
os.apiWithDialog('i/update', {
- pinnedPageId: pin ? page.id : null,
+ pinnedPageId: pin ? page.value.id : null,
});
}
-watch(() => path, fetchPage, { immediate: true });
+watch(() => path.value, fetchPage, { immediate: true });
-const headerActions = $computed(() => []);
+const headerActions = computed(() => []);
-const headerTabs = $computed(() => []);
+const headerTabs = computed(() => []);
-definePageMetadata(computed(() => page ? {
- title: page.title || page.name,
- avatar: page.user,
- path: `/@${page.user.username}/pages/${page.name}`,
+definePageMetadata(computed(() => page.value ? {
+ title: page.value.title || page.value.name,
+ avatar: page.value.user,
+ path: `/@${page.value.user.username}/pages/${page.value.name}`,
share: {
- title: page.title || page.name,
- text: page.summary,
+ title: page.value.title || page.value.name,
+ text: page.value.summary,
},
} : null));
</script>
diff --git a/packages/frontend/src/pages/pages.vue b/packages/frontend/src/pages/pages.vue
index b699f77b85..a7ca433ed3 100644
--- a/packages/frontend/src/pages/pages.vue
+++ b/packages/frontend/src/pages/pages.vue
@@ -36,7 +36,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { computed } from 'vue';
+import { computed, ref } from 'vue';
import MkPagePreview from '@/components/MkPagePreview.vue';
import MkPagination from '@/components/MkPagination.vue';
import MkButton from '@/components/MkButton.vue';
@@ -46,7 +46,7 @@ import { definePageMetadata } from '@/scripts/page-metadata.js';
const router = useRouter();
-let tab = $ref('featured');
+const tab = ref('featured');
const featuredPagesPagination = {
endpoint: 'pages/featured' as const,
@@ -65,13 +65,13 @@ function create() {
router.push('/pages/new');
}
-const headerActions = $computed(() => [{
+const headerActions = computed(() => [{
icon: 'ph-plus ph-bold ph-lg',
text: i18n.ts.create,
handler: create,
}]);
-const headerTabs = $computed(() => [{
+const headerTabs = computed(() => [{
key: 'featured',
title: i18n.ts._pages.featured,
icon: 'ph-fire ph-bold ph-lg',
diff --git a/packages/frontend/src/pages/registry.keys.vue b/packages/frontend/src/pages/registry.keys.vue
index c762110ead..7e4a0b508d 100644
--- a/packages/frontend/src/pages/registry.keys.vue
+++ b/packages/frontend/src/pages/registry.keys.vue
@@ -33,7 +33,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { watch } from 'vue';
+import { watch, computed, ref } from 'vue';
import JSON5 from 'json5';
import * as os from '@/os.js';
import { i18n } from '@/i18n.js';
@@ -49,16 +49,16 @@ const props = defineProps<{
domain: string;
}>();
-const scope = $computed(() => props.path ? props.path.split('/') : []);
+const scope = computed(() => props.path ? props.path.split('/') : []);
-let keys = $ref(null);
+const keys = ref(null);
function fetchKeys() {
os.api('i/registry/keys-with-type', {
- scope: scope,
+ scope: scope.value,
domain: props.domain === '@' ? null : props.domain,
}).then(res => {
- keys = Object.entries(res).sort((a, b) => a[0].localeCompare(b[0]));
+ keys.value = Object.entries(res).sort((a, b) => a[0].localeCompare(b[0]));
});
}
@@ -76,7 +76,7 @@ async function createKey() {
scope: {
type: 'string',
label: i18n.ts._registry.scope,
- default: scope.join('/'),
+ default: scope.value.join('/'),
},
});
if (canceled) return;
@@ -91,9 +91,9 @@ async function createKey() {
watch(() => props.path, fetchKeys, { immediate: true });
-const headerActions = $computed(() => []);
+const headerActions = computed(() => []);
-const headerTabs = $computed(() => []);
+const headerTabs = computed(() => []);
definePageMetadata({
title: i18n.ts.registry,
diff --git a/packages/frontend/src/pages/registry.value.vue b/packages/frontend/src/pages/registry.value.vue
index 7206ac7b46..baa88ec008 100644
--- a/packages/frontend/src/pages/registry.value.vue
+++ b/packages/frontend/src/pages/registry.value.vue
@@ -26,9 +26,9 @@ SPDX-License-Identifier: AGPL-3.0-only
</MkKeyValue>
</FormSplit>
- <MkTextarea v-model="valueForEditor" tall class="_monospace">
+ <MkCodeEditor v-model="valueForEditor" lang="json5">
<template #label>{{ i18n.ts.value }} (JSON)</template>
- </MkTextarea>
+ </MkCodeEditor>
<MkButton primary @click="save"><i class="ph-floppy-disk ph-bold ph-lg"></i> {{ i18n.ts.save }}</MkButton>
@@ -45,14 +45,14 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { watch } from 'vue';
+import { watch, computed, ref } from 'vue';
import JSON5 from 'json5';
import * as os from '@/os.js';
import { i18n } from '@/i18n.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';
import MkButton from '@/components/MkButton.vue';
import MkKeyValue from '@/components/MkKeyValue.vue';
-import MkTextarea from '@/components/MkTextarea.vue';
+import MkCodeEditor from '@/components/MkCodeEditor.vue';
import FormSplit from '@/components/form/split.vue';
import FormInfo from '@/components/MkInfo.vue';
@@ -61,26 +61,26 @@ const props = defineProps<{
domain: string;
}>();
-const scope = $computed(() => props.path.split('/').slice(0, -1));
-const key = $computed(() => props.path.split('/').at(-1));
+const scope = computed(() => props.path.split('/').slice(0, -1));
+const key = computed(() => props.path.split('/').at(-1));
-let value = $ref(null);
-let valueForEditor = $ref(null);
+const value = ref(null);
+const valueForEditor = ref(null);
function fetchValue() {
os.api('i/registry/get-detail', {
- scope,
- key,
+ scope: scope.value,
+ key: key.value,
domain: props.domain === '@' ? null : props.domain,
}).then(res => {
- value = res;
- valueForEditor = JSON5.stringify(res.value, null, '\t');
+ value.value = res;
+ valueForEditor.value = JSON5.stringify(res.value, null, '\t');
});
}
async function save() {
try {
- JSON5.parse(valueForEditor);
+ JSON5.parse(valueForEditor.value);
} catch (err) {
os.alert({
type: 'error',
@@ -94,9 +94,9 @@ async function save() {
}).then(({ canceled }) => {
if (canceled) return;
os.apiWithDialog('i/registry/set', {
- scope,
- key,
- value: JSON5.parse(valueForEditor),
+ scope: scope.value,
+ key: key.value,
+ value: JSON5.parse(valueForEditor.value),
domain: props.domain === '@' ? null : props.domain,
});
});
@@ -109,8 +109,8 @@ function del() {
}).then(({ canceled }) => {
if (canceled) return;
os.apiWithDialog('i/registry/remove', {
- scope,
- key,
+ scope: scope.value,
+ key: key.value,
domain: props.domain === '@' ? null : props.domain,
});
});
@@ -118,9 +118,9 @@ function del() {
watch(() => props.path, fetchValue, { immediate: true });
-const headerActions = $computed(() => []);
+const headerActions = computed(() => []);
-const headerTabs = $computed(() => []);
+const headerTabs = computed(() => []);
definePageMetadata({
title: i18n.ts.registry,
diff --git a/packages/frontend/src/pages/registry.vue b/packages/frontend/src/pages/registry.vue
index e462a21f9a..0572102849 100644
--- a/packages/frontend/src/pages/registry.vue
+++ b/packages/frontend/src/pages/registry.vue
@@ -22,6 +22,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
+import { ref, computed } from 'vue';
import JSON5 from 'json5';
import * as os from '@/os.js';
import { i18n } from '@/i18n.js';
@@ -30,11 +31,11 @@ import FormLink from '@/components/form/link.vue';
import FormSection from '@/components/form/section.vue';
import MkButton from '@/components/MkButton.vue';
-let scopesWithDomain = $ref(null);
+const scopesWithDomain = ref(null);
function fetchScopes() {
os.api('i/registry/scopes-with-domain').then(res => {
- scopesWithDomain = res;
+ scopesWithDomain.value = res;
});
}
@@ -66,9 +67,9 @@ async function createKey() {
fetchScopes();
-const headerActions = $computed(() => []);
+const headerActions = computed(() => []);
-const headerTabs = $computed(() => []);
+const headerTabs = computed(() => []);
definePageMetadata({
title: i18n.ts.registry,
diff --git a/packages/frontend/src/pages/reset-password.vue b/packages/frontend/src/pages/reset-password.vue
index c1fdfdbeb6..1aed57724e 100644
--- a/packages/frontend/src/pages/reset-password.vue
+++ b/packages/frontend/src/pages/reset-password.vue
@@ -20,7 +20,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { defineAsyncComponent, onMounted } from 'vue';
+import { defineAsyncComponent, onMounted, ref, computed } from 'vue';
import MkInput from '@/components/MkInput.vue';
import MkButton from '@/components/MkButton.vue';
import * as os from '@/os.js';
@@ -32,12 +32,12 @@ const props = defineProps<{
token?: string;
}>();
-let password = $ref('');
+const password = ref('');
async function save() {
await os.apiWithDialog('reset-password', {
token: props.token,
- password: password,
+ password: password.value,
});
mainRouter.push('/');
}
@@ -49,9 +49,9 @@ onMounted(() => {
}
});
-const headerActions = $computed(() => []);
+const headerActions = computed(() => []);
-const headerTabs = $computed(() => []);
+const headerTabs = computed(() => []);
definePageMetadata({
title: i18n.ts.resetPassword,
diff --git a/packages/frontend/src/pages/role.vue b/packages/frontend/src/pages/role.vue
index 193505f628..c99f66f012 100644
--- a/packages/frontend/src/pages/role.vue
+++ b/packages/frontend/src/pages/role.vue
@@ -36,7 +36,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { computed, watch } from 'vue';
+import { computed, watch, ref } from 'vue';
import * as os from '@/os.js';
import MkUserList from '@/components/MkUserList.vue';
import { definePageMetadata } from '@/scripts/page-metadata.js';
@@ -52,29 +52,29 @@ const props = withDefaults(defineProps<{
initialTab: 'users',
});
-let tab = $ref(props.initialTab);
-let role = $ref();
-let error = $ref();
-let visible = $ref(false);
+const tab = ref(props.initialTab);
+const role = ref();
+const error = ref();
+const visible = ref(false);
watch(() => props.role, () => {
os.api('roles/show', {
roleId: props.role,
}).then(res => {
- role = res;
- document.title = `${role?.name} | ${instanceName}`;
- visible = res.isExplorable && res.isPublic;
+ role.value = res;
+ document.title = `${role.value?.name} | ${instanceName}`;
+ visible.value = res.isExplorable && res.isPublic;
}).catch((err) => {
if (err.code === 'NO_SUCH_ROLE') {
- error = i18n.ts.noRole;
+ error.value = i18n.ts.noRole;
} else {
- error = i18n.ts.somethingHappened;
+ error.value = i18n.ts.somethingHappened;
}
- document.title = `${error} | ${instanceName}`;
+ document.title = `${error.value} | ${instanceName}`;
});
}, { immediate: true });
-const users = $computed(() => ({
+const users = computed(() => ({
endpoint: 'roles/users' as const,
limit: 30,
params: {
@@ -82,7 +82,7 @@ const users = $computed(() => ({
},
}));
-const headerTabs = $computed(() => [{
+const headerTabs = computed(() => [{
key: 'users',
icon: 'ph-users ph-bold ph-lg',
title: i18n.ts.users,
@@ -93,7 +93,7 @@ const headerTabs = $computed(() => [{
}]);
definePageMetadata(computed(() => ({
- title: role?.name,
+ title: role.value?.name,
icon: 'ph-seal-check ph-bold ph-lg',
})));
</script>
diff --git a/packages/frontend/src/pages/scratchpad.vue b/packages/frontend/src/pages/scratchpad.vue
index 6800481d55..ccda5bb8ac 100644
--- a/packages/frontend/src/pages/scratchpad.vue
+++ b/packages/frontend/src/pages/scratchpad.vue
@@ -39,7 +39,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { onDeactivated, onUnmounted, Ref, ref, watch } from 'vue';
+import { onDeactivated, onUnmounted, Ref, ref, watch, computed } from 'vue';
import { Interpreter, Parser, utils } from '@syuilo/aiscript';
import MkContainer from '@/components/MkContainer.vue';
import MkButton from '@/components/MkButton.vue';
@@ -59,8 +59,8 @@ let aiscript: Interpreter;
const code = ref('');
const logs = ref<any[]>([]);
const root = ref<AsUiRoot>();
-let components: Ref<AsUiComponent>[] = $ref([]);
-let uiKey = $ref(0);
+const components = ref<Ref<AsUiComponent>[]>([]);
+const uiKey = ref(0);
const saved = miLocalStorage.getItem('scratchpad');
if (saved) {
@@ -74,15 +74,15 @@ watch(code, () => {
async function run() {
if (aiscript) aiscript.abort();
root.value = undefined;
- components = [];
- uiKey++;
+ components.value = [];
+ uiKey.value++;
logs.value = [];
aiscript = new Interpreter(({
...createAiScriptEnv({
storageKey: 'widget',
token: $i?.token,
}),
- ...registerAsUiLib(components, (_root) => {
+ ...registerAsUiLib(components.value, (_root) => {
root.value = _root.value;
}),
}), {
@@ -160,9 +160,9 @@ onUnmounted(() => {
if (aiscript) aiscript.abort();
});
-const headerActions = $computed(() => []);
+const headerActions = computed(() => []);
-const headerTabs = $computed(() => []);
+const headerTabs = computed(() => []);
definePageMetadata({
title: i18n.ts.scratchpad,
diff --git a/packages/frontend/src/pages/search.note.vue b/packages/frontend/src/pages/search.note.vue
index 0b1740847d..acfa5b9fdf 100644
--- a/packages/frontend/src/pages/search.note.vue
+++ b/packages/frontend/src/pages/search.note.vue
@@ -50,41 +50,37 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { computed, onMounted } from 'vue';
+import { ref } from 'vue';
import MkNotes from '@/components/MkNotes.vue';
import MkInput from '@/components/MkInput.vue';
-import MkRadios from '@/components/MkRadios.vue';
import MkButton from '@/components/MkButton.vue';
import MkSwitch from '@/components/MkSwitch.vue';
import MkSelect from '@/components/MkSelect.vue';
import { i18n } from '@/i18n.js';
import * as os from '@/os.js';
import MkFoldableSection from '@/components/MkFoldableSection.vue';
-import { $i } from '@/account.js';
-import { instance } from '@/instance.js';
-import MkInfo from '@/components/MkInfo.vue';
import { useRouter } from '@/router.js';
import MkFolder from '@/components/MkFolder.vue';
const router = useRouter();
-let key = $ref(0);
-let searchQuery = $ref('');
-let searchOrigin = $ref('combined');
-let notePagination = $ref();
-let user = $ref(null);
-let isLocalOnly = $ref(false);
-let order = $ref(true);
-let filetype = $ref(null);
+const key = ref(0);
+const searchQuery = ref('');
+const searchOrigin = ref('combined');
+const notePagination = ref();
+const user = ref(null);
+const isLocalOnly = ref(false);
+const order = ref(false);
+const filetype = ref(null);
function selectUser() {
os.selectUser().then(_user => {
- user = _user;
+ user.value = _user;
});
}
async function search() {
- const query = searchQuery.toString().trim();
+ const query = searchQuery.value.toString().trim();
if (query == null || query === '') return;
@@ -106,19 +102,19 @@ async function search() {
return;
}
- notePagination = {
+ notePagination.value = {
endpoint: 'notes/search',
limit: 10,
params: {
- query: searchQuery,
- userId: user ? user.id : null,
- order: order ? 'desc' : 'asc',
- filetype: filetype,
+ query: searchQuery.value,
+ userId: user.value ? user.value.id : null,
+ order: order.value ? 'desc' : 'asc',
+ filetype: filetype.value,
},
};
- if (isLocalOnly) notePagination.params.host = '.';
+ if (isLocalOnly.value) notePagination.value.params.host = '.';
- key++;
+ key.value++;
}
</script>
diff --git a/packages/frontend/src/pages/search.user.vue b/packages/frontend/src/pages/search.user.vue
index c0b2e55986..0485012fdb 100644
--- a/packages/frontend/src/pages/search.user.vue
+++ b/packages/frontend/src/pages/search.user.vue
@@ -25,7 +25,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { computed, defineAsyncComponent, onMounted } from 'vue';
+import { ref } from 'vue';
import MkUserList from '@/components/MkUserList.vue';
import MkInput from '@/components/MkInput.vue';
import MkRadios from '@/components/MkRadios.vue';
@@ -33,20 +33,17 @@ import MkButton from '@/components/MkButton.vue';
import { i18n } from '@/i18n.js';
import * as os from '@/os.js';
import MkFoldableSection from '@/components/MkFoldableSection.vue';
-import { $i } from '@/account.js';
-import { instance } from '@/instance.js';
-import MkInfo from '@/components/MkInfo.vue';
import { useRouter } from '@/router.js';
const router = useRouter();
-let key = $ref('');
-let searchQuery = $ref('');
-let searchOrigin = $ref('combined');
-let userPagination = $ref();
+const key = ref('');
+const searchQuery = ref('');
+const searchOrigin = ref('combined');
+const userPagination = ref();
async function search() {
- const query = searchQuery.toString().trim();
+ const query = searchQuery.value.toString().trim();
if (query == null || query === '') return;
@@ -68,15 +65,15 @@ async function search() {
return;
}
- userPagination = {
+ userPagination.value = {
endpoint: 'users/search',
limit: 10,
params: {
query: query,
- origin: searchOrigin,
+ origin: searchOrigin.value,
},
};
- key = query;
+ key.value = query;
}
</script>
diff --git a/packages/frontend/src/pages/search.vue b/packages/frontend/src/pages/search.vue
index eae120cb34..acc291c73e 100644
--- a/packages/frontend/src/pages/search.vue
+++ b/packages/frontend/src/pages/search.vue
@@ -23,10 +23,9 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { computed, defineAsyncComponent, onMounted } from 'vue';
+import { computed, defineAsyncComponent, ref } from 'vue';
import { i18n } from '@/i18n.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';
-import * as os from '@/os.js';
import { $i } from '@/account.js';
import { instance } from '@/instance.js';
import MkInfo from '@/components/MkInfo.vue';
@@ -34,13 +33,13 @@ import MkInfo from '@/components/MkInfo.vue';
const XNote = defineAsyncComponent(() => import('./search.note.vue'));
const XUser = defineAsyncComponent(() => import('./search.user.vue'));
-let tab = $ref('note');
+const tab = ref('note');
const notesSearchAvailable = (($i == null && instance.policies.canSearchNotes) || ($i != null && $i.policies.canSearchNotes));
-const headerActions = $computed(() => []);
+const headerActions = computed(() => []);
-const headerTabs = $computed(() => [{
+const headerTabs = computed(() => [{
key: 'note',
title: i18n.ts.notes,
icon: 'ph-pencil ph-bold ph-lg',
diff --git a/packages/frontend/src/pages/settings/2fa.vue b/packages/frontend/src/pages/settings/2fa.vue
index 4e2f367374..09421ba2c2 100644
--- a/packages/frontend/src/pages/settings/2fa.vue
+++ b/packages/frontend/src/pages/settings/2fa.vue
@@ -72,7 +72,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { ref, defineAsyncComponent } from 'vue';
+import { defineAsyncComponent, computed } from 'vue';
import { supported as webAuthnSupported, create as webAuthnCreate, parseCreationOptionsFromJSON } from '@github/webauthn-json/browser-ponyfill';
import MkButton from '@/components/MkButton.vue';
import MkInfo from '@/components/MkInfo.vue';
@@ -91,7 +91,7 @@ withDefaults(defineProps<{
first: false,
});
-const usePasswordLessLogin = $computed(() => $i?.usePasswordLessLogin ?? false);
+const usePasswordLessLogin = computed(() => $i?.usePasswordLessLogin ?? false);
async function registerTOTP(): Promise<void> {
const auth = await os.authenticateDialog();
diff --git a/packages/frontend/src/pages/settings/accounts.vue b/packages/frontend/src/pages/settings/accounts.vue
index 5bce040cef..642100342e 100644
--- a/packages/frontend/src/pages/settings/accounts.vue
+++ b/packages/frontend/src/pages/settings/accounts.vue
@@ -19,7 +19,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { defineAsyncComponent, ref } from 'vue';
+import { defineAsyncComponent, ref, computed } from 'vue';
import type * as Misskey from 'misskey-js';
import FormSuspense from '@/components/form/suspense.vue';
import MkButton from '@/components/MkButton.vue';
@@ -101,9 +101,9 @@ function switchAccountWithToken(token: string) {
login(token);
}
-const headerActions = $computed(() => []);
+const headerActions = computed(() => []);
-const headerTabs = $computed(() => []);
+const headerTabs = computed(() => []);
definePageMetadata({
title: i18n.ts.accounts,
diff --git a/packages/frontend/src/pages/settings/api.vue b/packages/frontend/src/pages/settings/api.vue
index 2f03d07c6e..ca38bd2e3d 100644
--- a/packages/frontend/src/pages/settings/api.vue
+++ b/packages/frontend/src/pages/settings/api.vue
@@ -12,7 +12,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { defineAsyncComponent, ref } from 'vue';
+import { defineAsyncComponent, ref, computed } from 'vue';
import FormLink from '@/components/form/link.vue';
import MkButton from '@/components/MkButton.vue';
import * as os from '@/os.js';
@@ -40,9 +40,9 @@ function generateToken() {
}, 'closed');
}
-const headerActions = $computed(() => []);
+const headerActions = computed(() => []);
-const headerTabs = $computed(() => []);
+const headerTabs = computed(() => []);
definePageMetadata({
title: 'API',
diff --git a/packages/frontend/src/pages/settings/apps.vue b/packages/frontend/src/pages/settings/apps.vue
index db8dcd82f5..424d9fd4c9 100644
--- a/packages/frontend/src/pages/settings/apps.vue
+++ b/packages/frontend/src/pages/settings/apps.vue
@@ -45,7 +45,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { ref } from 'vue';
+import { ref, computed } from 'vue';
import FormPagination from '@/components/MkPagination.vue';
import * as os from '@/os.js';
import { i18n } from '@/i18n.js';
@@ -71,9 +71,9 @@ function revoke(token) {
});
}
-const headerActions = $computed(() => []);
+const headerActions = computed(() => []);
-const headerTabs = $computed(() => []);
+const headerTabs = computed(() => []);
definePageMetadata({
title: i18n.ts.installedApps,
diff --git a/packages/frontend/src/pages/settings/avatar-decoration.decoration.vue b/packages/frontend/src/pages/settings/avatar-decoration.decoration.vue
new file mode 100644
index 0000000000..9c95b5547e
--- /dev/null
+++ b/packages/frontend/src/pages/settings/avatar-decoration.decoration.vue
@@ -0,0 +1,69 @@
+<!--
+SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-License-Identifier: AGPL-3.0-only
+-->
+
+<template>
+<div
+ :class="[$style.root, { [$style.active]: active }]"
+ @click="emit('click')"
+>
+ <div :class="$style.name"><MkCondensedLine :minScale="0.5">{{ decoration.name }}</MkCondensedLine></div>
+ <MkAvatar style="width: 60px; height: 60px;" :user="$i" :decorations="[{ url: decoration.url, angle, flipH, offsetX, offsetY }]" forceShowDecoration/>
+ <i v-if="decoration.roleIdsThatCanBeUsedThisDecoration.length > 0 && !$i.roles.some(r => decoration.roleIdsThatCanBeUsedThisDecoration.includes(r.id))" :class="$style.lock" class="ti ti-lock"></i>
+</div>
+</template>
+
+<script lang="ts" setup>
+import { } from 'vue';
+import { $i } from '@/account.js';
+
+const props = defineProps<{
+ active?: boolean;
+ decoration: {
+ id: string;
+ url: string;
+ name: string;
+ roleIdsThatCanBeUsedThisDecoration: string[];
+ };
+ angle?: number;
+ flipH?: boolean;
+ offsetX?: number;
+ offsetY?: number;
+}>();
+
+const emit = defineEmits<{
+ (ev: 'click'): void;
+}>();
+</script>
+
+<style lang="scss" module>
+.root {
+ cursor: pointer;
+ padding: 16px 16px 28px 16px;
+ border: solid 2px var(--divider);
+ border-radius: 8px;
+ text-align: center;
+ font-size: 90%;
+ overflow: clip;
+ contain: content;
+}
+
+.active {
+ background-color: var(--accentedBg);
+ border-color: var(--accent);
+}
+
+.name {
+ position: relative;
+ z-index: 10;
+ font-weight: bold;
+ margin-bottom: 20px;
+}
+
+.lock {
+ position: absolute;
+ bottom: 12px;
+ right: 12px;
+}
+</style>
diff --git a/packages/frontend/src/pages/settings/avatar-decoration.dialog.vue b/packages/frontend/src/pages/settings/avatar-decoration.dialog.vue
new file mode 100644
index 0000000000..329ab4d47a
--- /dev/null
+++ b/packages/frontend/src/pages/settings/avatar-decoration.dialog.vue
@@ -0,0 +1,154 @@
+<!--
+SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-License-Identifier: AGPL-3.0-only
+-->
+
+<template>
+<MkModalWindow
+ ref="dialog"
+ :width="400"
+ :height="450"
+ @close="cancel"
+ @closed="emit('closed')"
+>
+ <template #header>{{ i18n.ts.avatarDecorations }}</template>
+
+ <div>
+ <MkSpacer :marginMin="20" :marginMax="28">
+ <div style="text-align: center;">
+ <div :class="$style.name">{{ decoration.name }}</div>
+ <MkAvatar style="width: 64px; height: 64px; margin-bottom: 20px;" :user="$i" :decorations="decorationsForPreview" forceShowDecoration/>
+ </div>
+ <div class="_gaps_s">
+ <MkRange v-model="angle" continuousUpdate :min="-0.5" :max="0.5" :step="0.025" :textConverter="(v) => `${Math.floor(v * 360)}°`">
+ <template #label>{{ i18n.ts.angle }}</template>
+ </MkRange>
+ <MkRange v-model="offsetX" continuousUpdate :min="-0.25" :max="0.25" :step="0.025" :textConverter="(v) => `${Math.floor(v * 100)}%`">
+ <template #label>X {{ i18n.ts.position }}</template>
+ </MkRange>
+ <MkRange v-model="offsetY" continuousUpdate :min="-0.25" :max="0.25" :step="0.025" :textConverter="(v) => `${Math.floor(v * 100)}%`">
+ <template #label>Y {{ i18n.ts.position }}</template>
+ </MkRange>
+ <MkSwitch v-model="flipH">
+ <template #label>{{ i18n.ts.flip }}</template>
+ </MkSwitch>
+ </div>
+ </MkSpacer>
+
+ <div :class="$style.footer" class="_buttonsCenter">
+ <MkButton v-if="usingIndex != null" primary rounded @click="update"><i class="ti ti-check"></i> {{ i18n.ts.update }}</MkButton>
+ <MkButton v-if="usingIndex != null" rounded @click="detach"><i class="ti ti-x"></i> {{ i18n.ts.detach }}</MkButton>
+ <MkButton v-else :disabled="exceeded" primary rounded @click="attach"><i class="ti ti-check"></i> {{ i18n.ts.attach }}</MkButton>
+ </div>
+ </div>
+</MkModalWindow>
+</template>
+
+<script lang="ts" setup>
+import { shallowRef, ref, computed } from 'vue';
+import MkButton from '@/components/MkButton.vue';
+import MkModalWindow from '@/components/MkModalWindow.vue';
+import MkSwitch from '@/components/MkSwitch.vue';
+import { i18n } from '@/i18n.js';
+import MkRange from '@/components/MkRange.vue';
+import { $i } from '@/account.js';
+
+const props = defineProps<{
+ usingIndex: number | null;
+ decoration: {
+ id: string;
+ url: string;
+ name: string;
+ };
+}>();
+
+const emit = defineEmits<{
+ (ev: 'closed'): void;
+ (ev: 'attach', payload: {
+ angle: number;
+ flipH: boolean;
+ offsetX: number;
+ offsetY: number;
+ }): void;
+ (ev: 'update', payload: {
+ angle: number;
+ flipH: boolean;
+ offsetX: number;
+ offsetY: number;
+ }): void;
+ (ev: 'detach'): void;
+}>();
+
+const dialog = shallowRef<InstanceType<typeof MkModalWindow>>();
+const exceeded = computed(() => ($i.policies.avatarDecorationLimit - $i.avatarDecorations.length) <= 0);
+const angle = ref((props.usingIndex != null ? $i.avatarDecorations[props.usingIndex].angle : null) ?? 0);
+const flipH = ref((props.usingIndex != null ? $i.avatarDecorations[props.usingIndex].flipH : null) ?? false);
+const offsetX = ref((props.usingIndex != null ? $i.avatarDecorations[props.usingIndex].offsetX : null) ?? 0);
+const offsetY = ref((props.usingIndex != null ? $i.avatarDecorations[props.usingIndex].offsetY : null) ?? 0);
+
+const decorationsForPreview = computed(() => {
+ const decoration = {
+ id: props.decoration.id,
+ url: props.decoration.url,
+ angle: angle.value,
+ flipH: flipH.value,
+ offsetX: offsetX.value,
+ offsetY: offsetY.value,
+ };
+ const decorations = [...$i.avatarDecorations];
+ if (props.usingIndex != null) {
+ decorations[props.usingIndex] = decoration;
+ } else {
+ decorations.push(decoration);
+ }
+ return decorations;
+});
+
+function cancel() {
+ dialog.value.close();
+}
+
+async function update() {
+ emit('update', {
+ angle: angle.value,
+ flipH: flipH.value,
+ offsetX: offsetX.value,
+ offsetY: offsetY.value,
+ });
+ dialog.value.close();
+}
+
+async function attach() {
+ emit('attach', {
+ angle: angle.value,
+ flipH: flipH.value,
+ offsetX: offsetX.value,
+ offsetY: offsetY.value,
+ });
+ dialog.value.close();
+}
+
+async function detach() {
+ emit('detach');
+ dialog.value.close();
+}
+</script>
+
+<style lang="scss" module>
+.name {
+ position: relative;
+ z-index: 10;
+ font-weight: bold;
+ margin-bottom: 28px;
+}
+
+.footer {
+ position: sticky;
+ bottom: 0;
+ left: 0;
+ padding: 12px;
+ border-top: solid 0.5px var(--divider);
+ -webkit-backdrop-filter: var(--blur, blur(15px));
+ backdrop-filter: var(--blur, blur(15px));
+}
+</style>
diff --git a/packages/frontend/src/pages/settings/avatar-decoration.vue b/packages/frontend/src/pages/settings/avatar-decoration.vue
new file mode 100644
index 0000000000..6551fc917e
--- /dev/null
+++ b/packages/frontend/src/pages/settings/avatar-decoration.vue
@@ -0,0 +1,152 @@
+<!--
+SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-License-Identifier: AGPL-3.0-only
+-->
+
+<template>
+<div>
+ <div v-if="!loading" class="_gaps">
+ <MkInfo>{{ i18n.t('_profile.avatarDecorationMax', { max: $i.policies.avatarDecorationLimit }) }} ({{ i18n.t('remainingN', { n: $i.policies.avatarDecorationLimit - $i.avatarDecorations.length }) }})</MkInfo>
+
+ <MkAvatar :class="$style.avatar" :user="$i" forceShowDecoration/>
+
+ <div v-if="$i.avatarDecorations.length > 0" v-panel :class="$style.current" class="_gaps_s">
+ <div>{{ i18n.ts.inUse }}</div>
+
+ <div :class="$style.decorations">
+ <XDecoration
+ v-for="(avatarDecoration, i) in $i.avatarDecorations"
+ :decoration="avatarDecorations.find(d => d.id === avatarDecoration.id)"
+ :angle="avatarDecoration.angle"
+ :flipH="avatarDecoration.flipH"
+ :offsetX="avatarDecoration.offsetX"
+ :offsetY="avatarDecoration.offsetY"
+ :active="true"
+ @click="openDecoration(avatarDecoration, i)"
+ />
+ </div>
+
+ <MkButton danger @click="detachAllDecorations">{{ i18n.ts.detachAll }}</MkButton>
+ </div>
+
+ <div :class="$style.decorations">
+ <XDecoration
+ v-for="avatarDecoration in avatarDecorations"
+ :key="avatarDecoration.id"
+ :decoration="avatarDecoration"
+ @click="openDecoration(avatarDecoration)"
+ />
+ </div>
+ </div>
+ <div v-else>
+ <MkLoading/>
+ </div>
+</div>
+</template>
+
+<script lang="ts" setup>
+import { ref, defineAsyncComponent, computed } from 'vue';
+import * as Misskey from 'misskey-js';
+import XDecoration from './avatar-decoration.decoration.vue';
+import MkButton from '@/components/MkButton.vue';
+import * as os from '@/os.js';
+import { i18n } from '@/i18n.js';
+import { $i } from '@/account.js';
+import MkInfo from '@/components/MkInfo.vue';
+import { definePageMetadata } from '@/scripts/page-metadata.js';
+
+const loading = ref(true);
+const avatarDecorations = ref<Misskey.entities.GetAvatarDecorationsResponse>([]);
+
+os.api('get-avatar-decorations').then(_avatarDecorations => {
+ avatarDecorations.value = _avatarDecorations;
+ loading.value = false;
+});
+
+function openDecoration(avatarDecoration, index?: number) {
+ os.popup(defineAsyncComponent(() => import('./avatar-decoration.dialog.vue')), {
+ decoration: avatarDecoration,
+ usingIndex: index,
+ }, {
+ 'attach': async (payload) => {
+ const decoration = {
+ id: avatarDecoration.id,
+ angle: payload.angle,
+ flipH: payload.flipH,
+ offsetX: payload.offsetX,
+ offsetY: payload.offsetY,
+ };
+ const update = [...$i.avatarDecorations, decoration];
+ await os.apiWithDialog('i/update', {
+ avatarDecorations: update,
+ });
+ $i.avatarDecorations = update;
+ },
+ 'update': async (payload) => {
+ const decoration = {
+ id: avatarDecoration.id,
+ angle: payload.angle,
+ flipH: payload.flipH,
+ offsetX: payload.offsetX,
+ offsetY: payload.offsetY,
+ };
+ const update = [...$i.avatarDecorations];
+ update[index] = decoration;
+ await os.apiWithDialog('i/update', {
+ avatarDecorations: update,
+ });
+ $i.avatarDecorations = update;
+ },
+ 'detach': async () => {
+ const update = [...$i.avatarDecorations];
+ update.splice(index, 1);
+ await os.apiWithDialog('i/update', {
+ avatarDecorations: update,
+ });
+ $i.avatarDecorations = update;
+ },
+ }, 'closed');
+}
+
+function detachAllDecorations() {
+ os.confirm({
+ type: 'warning',
+ text: i18n.ts.areYouSure,
+ }).then(async ({ canceled }) => {
+ if (canceled) return;
+ await os.apiWithDialog('i/update', {
+ avatarDecorations: [],
+ });
+ $i.avatarDecorations = [];
+ });
+}
+
+const headerActions = computed(() => []);
+
+const headerTabs = computed(() => []);
+
+definePageMetadata({
+ title: i18n.ts.avatarDecorations,
+ icon: 'ti ti-sparkles',
+});
+</script>
+
+<style lang="scss" module>
+.avatar {
+ display: inline-block;
+ width: 72px;
+ height: 72px;
+ margin: 16px auto;
+}
+
+.current {
+ padding: 16px;
+ border-radius: var(--radius);
+}
+
+.decorations {
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
+ grid-gap: 12px;
+}
+</style>
diff --git a/packages/frontend/src/pages/settings/custom-css.vue b/packages/frontend/src/pages/settings/custom-css.vue
index 6e1481ae97..00a1fca856 100644
--- a/packages/frontend/src/pages/settings/custom-css.vue
+++ b/packages/frontend/src/pages/settings/custom-css.vue
@@ -7,15 +7,15 @@ SPDX-License-Identifier: AGPL-3.0-only
<div class="_gaps_m">
<FormInfo warn>{{ i18n.ts.customCssWarn }}</FormInfo>
- <MkTextarea v-model="localCustomCss" manualSave tall class="_monospace" style="tab-size: 2;">
+ <MkCodeEditor v-model="localCustomCss" manualSave lang="css">
<template #label>CSS</template>
- </MkTextarea>
+ </MkCodeEditor>
</div>
</template>
<script lang="ts" setup>
-import { ref, watch } from 'vue';
-import MkTextarea from '@/components/MkTextarea.vue';
+import { ref, watch, computed } from 'vue';
+import MkCodeEditor from '@/components/MkCodeEditor.vue';
import FormInfo from '@/components/MkInfo.vue';
import * as os from '@/os.js';
import { unisonReload } from '@/scripts/unison-reload.js';
@@ -41,9 +41,9 @@ watch(localCustomCss, async () => {
await apply();
});
-const headerActions = $computed(() => []);
+const headerActions = computed(() => []);
-const headerTabs = $computed(() => []);
+const headerTabs = computed(() => []);
definePageMetadata({
title: i18n.ts.customCss,
diff --git a/packages/frontend/src/pages/settings/deck.vue b/packages/frontend/src/pages/settings/deck.vue
index ff9a78cd52..32acd5e7a6 100644
--- a/packages/frontend/src/pages/settings/deck.vue
+++ b/packages/frontend/src/pages/settings/deck.vue
@@ -32,9 +32,9 @@ const useSimpleUiForNonRootPages = computed(deckStore.makeGetterSetter('useSimpl
const alwaysShowMainColumn = computed(deckStore.makeGetterSetter('alwaysShowMainColumn'));
const columnAlign = computed(deckStore.makeGetterSetter('columnAlign'));
-const headerActions = $computed(() => []);
+const headerActions = computed(() => []);
-const headerTabs = $computed(() => []);
+const headerTabs = computed(() => []);
definePageMetadata({
title: i18n.ts.deck,
diff --git a/packages/frontend/src/pages/settings/drive-cleaner.vue b/packages/frontend/src/pages/settings/drive-cleaner.vue
index 00dc022259..601479b73c 100644
--- a/packages/frontend/src/pages/settings/drive-cleaner.vue
+++ b/packages/frontend/src/pages/settings/drive-cleaner.vue
@@ -55,12 +55,11 @@ import MkPagination from '@/components/MkPagination.vue';
import MkDriveFileThumbnail from '@/components/MkDriveFileThumbnail.vue';
import { i18n } from '@/i18n.js';
import bytes from '@/filters/bytes.js';
-import { dateString } from '@/filters/date.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';
import MkSelect from '@/components/MkSelect.vue';
import { getDriveFileMenu } from '@/scripts/get-drive-file-menu.js';
-let sortMode = ref('+size');
+const sortMode = ref('+size');
const pagination = {
endpoint: 'drive/files' as const,
limit: 10,
diff --git a/packages/frontend/src/pages/settings/drive.vue b/packages/frontend/src/pages/settings/drive.vue
index 5b9bcce944..fc2ce45bf0 100644
--- a/packages/frontend/src/pages/settings/drive.vue
+++ b/packages/frontend/src/pages/settings/drive.vue
@@ -72,7 +72,7 @@ const fetching = ref(true);
const usage = ref<any>(null);
const capacity = ref<any>(null);
const uploadFolder = ref<any>(null);
-let alwaysMarkNsfw = $ref($i.alwaysMarkNsfw);
+const alwaysMarkNsfw = ref($i.alwaysMarkNsfw);
const meterStyle = computed(() => {
return {
@@ -117,20 +117,20 @@ function chooseUploadFolder() {
function saveProfile() {
os.api('i/update', {
- alwaysMarkNsfw: !!alwaysMarkNsfw,
+ alwaysMarkNsfw: !!alwaysMarkNsfw.value,
}).catch(err => {
os.alert({
type: 'error',
title: i18n.ts.error,
text: err.message,
});
- alwaysMarkNsfw = true;
+ alwaysMarkNsfw.value = true;
});
}
-const headerActions = $computed(() => []);
+const headerActions = computed(() => []);
-const headerTabs = $computed(() => []);
+const headerTabs = computed(() => []);
definePageMetadata({
title: i18n.ts.drive,
diff --git a/packages/frontend/src/pages/settings/email.vue b/packages/frontend/src/pages/settings/email.vue
index 6e7d78292f..003501f45a 100644
--- a/packages/frontend/src/pages/settings/email.vue
+++ b/packages/frontend/src/pages/settings/email.vue
@@ -48,7 +48,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { onMounted, ref, watch } from 'vue';
+import { onMounted, ref, watch, computed } from 'vue';
import FormSection from '@/components/form/section.vue';
import MkInfo from '@/components/MkInfo.vue';
import MkInput from '@/components/MkInput.vue';
@@ -106,9 +106,9 @@ onMounted(() => {
});
});
-const headerActions = $computed(() => []);
+const headerActions = computed(() => []);
-const headerTabs = $computed(() => []);
+const headerTabs = computed(() => []);
definePageMetadata({
title: i18n.ts.email,
diff --git a/packages/frontend/src/pages/settings/emoji-picker.vue b/packages/frontend/src/pages/settings/emoji-picker.vue
new file mode 100644
index 0000000000..61f3332122
--- /dev/null
+++ b/packages/frontend/src/pages/settings/emoji-picker.vue
@@ -0,0 +1,274 @@
+<!--
+SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-License-Identifier: AGPL-3.0-only
+-->
+
+<template>
+<div class="_gaps_m">
+ <MkFolder :defaultOpen="true">
+ <template #icon><i class="ti ti-pin"></i></template>
+ <template #label>{{ i18n.ts.pinned }} ({{ i18n.ts.reaction }})</template>
+ <template #caption>{{ i18n.ts.pinnedEmojisForReactionSettingDescription }}</template>
+
+ <div class="_gaps">
+ <div>
+ <div v-panel style="border-radius: 6px;">
+ <Sortable
+ v-model="pinnedEmojisForReaction"
+ :class="$style.emojis"
+ :itemKey="item => item"
+ :animation="150"
+ :delay="100"
+ :delayOnTouchOnly="true"
+ >
+ <template #item="{element}">
+ <button class="_button" :class="$style.emojisItem" @click="removeReaction(element, $event)">
+ <MkCustomEmoji v-if="element[0] === ':'" :name="element" :normal="true"/>
+ <MkEmoji v-else :emoji="element" :normal="true"/>
+ </button>
+ </template>
+ <template #footer>
+ <button class="_button" :class="$style.emojisAdd" @click="chooseReaction">
+ <i class="ti ti-plus"></i>
+ </button>
+ </template>
+ </Sortable>
+ </div>
+ <div :class="$style.editorCaption">{{ i18n.ts.reactionSettingDescription2 }}</div>
+ </div>
+
+ <div class="_buttons">
+ <MkButton inline @click="previewReaction"><i class="ti ti-eye"></i> {{ i18n.ts.preview }}</MkButton>
+ <MkButton inline danger @click="setDefaultReaction"><i class="ti ti-reload"></i> {{ i18n.ts.default }}</MkButton>
+ <MkButton inline danger @click="overwriteFromPinnedEmojis"><i class="ti ti-copy"></i> {{ i18n.ts.overwriteFromPinnedEmojis }}</MkButton>
+ </div>
+ </div>
+ </MkFolder>
+
+ <MkFolder>
+ <template #icon><i class="ti ti-pin"></i></template>
+ <template #label>{{ i18n.ts.pinned }} ({{ i18n.ts.general }})</template>
+ <template #caption>{{ i18n.ts.pinnedEmojisSettingDescription }}</template>
+
+ <div class="_gaps">
+ <div>
+ <div v-panel style="border-radius: 6px;">
+ <Sortable
+ v-model="pinnedEmojis"
+ :class="$style.emojis"
+ :itemKey="item => item"
+ :animation="150"
+ :delay="100"
+ :delayOnTouchOnly="true"
+ >
+ <template #item="{element}">
+ <button class="_button" :class="$style.emojisItem" @click="removeEmoji(element, $event)">
+ <MkCustomEmoji v-if="element[0] === ':'" :name="element" :normal="true"/>
+ <MkEmoji v-else :emoji="element" :normal="true"/>
+ </button>
+ </template>
+ <template #footer>
+ <button class="_button" :class="$style.emojisAdd" @click="chooseEmoji">
+ <i class="ti ti-plus"></i>
+ </button>
+ </template>
+ </Sortable>
+ </div>
+ <div :class="$style.editorCaption">{{ i18n.ts.reactionSettingDescription2 }}</div>
+ </div>
+
+ <div class="_buttons">
+ <MkButton inline @click="previewEmoji"><i class="ti ti-eye"></i> {{ i18n.ts.preview }}</MkButton>
+ <MkButton inline danger @click="setDefaultEmoji"><i class="ti ti-reload"></i> {{ i18n.ts.default }}</MkButton>
+ <MkButton inline danger @click="overwriteFromPinnedEmojisForReaction"><i class="ti ti-copy"></i> {{ i18n.ts.overwriteFromPinnedEmojisForReaction }}</MkButton>
+ </div>
+ </div>
+ </MkFolder>
+
+ <FormSection>
+ <template #label>{{ i18n.ts.emojiPickerDisplay }}</template>
+
+ <div class="_gaps_m">
+ <MkRadios v-model="emojiPickerScale">
+ <template #label>{{ i18n.ts.size }}</template>
+ <option :value="1">{{ i18n.ts.small }}</option>
+ <option :value="2">{{ i18n.ts.medium }}</option>
+ <option :value="3">{{ i18n.ts.large }}</option>
+ </MkRadios>
+
+ <MkRadios v-model="emojiPickerWidth">
+ <template #label>{{ i18n.ts.numberOfColumn }}</template>
+ <option :value="1">5</option>
+ <option :value="2">6</option>
+ <option :value="3">7</option>
+ <option :value="4">8</option>
+ <option :value="5">9</option>
+ </MkRadios>
+
+ <MkRadios v-model="emojiPickerHeight">
+ <template #label>{{ i18n.ts.height }}</template>
+ <option :value="1">{{ i18n.ts.small }}</option>
+ <option :value="2">{{ i18n.ts.medium }}</option>
+ <option :value="3">{{ i18n.ts.large }}</option>
+ <option :value="4">{{ i18n.ts.large }}+</option>
+ </MkRadios>
+
+ <MkSwitch v-model="emojiPickerUseDrawerForMobile">
+ {{ i18n.ts.useDrawerReactionPickerForMobile }}
+ <template #caption>{{ i18n.ts.needReloadToApply }}</template>
+ </MkSwitch>
+ </div>
+ </FormSection>
+</div>
+</template>
+
+<script lang="ts" setup>
+import { computed, ref, Ref, watch } from 'vue';
+import Sortable from 'vuedraggable';
+import MkRadios from '@/components/MkRadios.vue';
+import MkButton from '@/components/MkButton.vue';
+import FormSection from '@/components/form/section.vue';
+import MkSwitch from '@/components/MkSwitch.vue';
+import * as os from '@/os.js';
+import { defaultStore } from '@/store.js';
+import { i18n } from '@/i18n.js';
+import { definePageMetadata } from '@/scripts/page-metadata.js';
+import { deepClone } from '@/scripts/clone.js';
+import { reactionPicker } from '@/scripts/reaction-picker.js';
+import { emojiPicker } from '@/scripts/emoji-picker.js';
+import MkCustomEmoji from '@/components/global/MkCustomEmoji.vue';
+import MkEmoji from '@/components/global/MkEmoji.vue';
+import MkFolder from '@/components/MkFolder.vue';
+
+const pinnedEmojisForReaction: Ref<string[]> = ref(deepClone(defaultStore.state.reactions));
+const pinnedEmojis: Ref<string[]> = ref(deepClone(defaultStore.state.pinnedEmojis));
+
+const emojiPickerScale = computed(defaultStore.makeGetterSetter('emojiPickerScale'));
+const emojiPickerWidth = computed(defaultStore.makeGetterSetter('emojiPickerWidth'));
+const emojiPickerHeight = computed(defaultStore.makeGetterSetter('emojiPickerHeight'));
+const emojiPickerUseDrawerForMobile = computed(defaultStore.makeGetterSetter('emojiPickerUseDrawerForMobile'));
+
+const removeReaction = (reaction: string, ev: MouseEvent) => remove(pinnedEmojisForReaction, reaction, ev);
+const chooseReaction = (ev: MouseEvent) => pickEmoji(pinnedEmojisForReaction, ev);
+const setDefaultReaction = () => setDefault(pinnedEmojisForReaction);
+
+const removeEmoji = (reaction: string, ev: MouseEvent) => remove(pinnedEmojis, reaction, ev);
+const chooseEmoji = (ev: MouseEvent) => pickEmoji(pinnedEmojis, ev);
+const setDefaultEmoji = () => setDefault(pinnedEmojis);
+
+function previewReaction(ev: MouseEvent) {
+ reactionPicker.show(getHTMLElement(ev));
+}
+
+function previewEmoji(ev: MouseEvent) {
+ emojiPicker.show(getHTMLElement(ev));
+}
+
+async function overwriteFromPinnedEmojis() {
+ const { canceled } = await os.confirm({
+ type: 'warning',
+ text: i18n.ts.overwriteContentConfirm,
+ });
+
+ if (canceled) {
+ return;
+ }
+
+ pinnedEmojisForReaction.value = [...pinnedEmojis.value];
+}
+
+async function overwriteFromPinnedEmojisForReaction() {
+ const { canceled } = await os.confirm({
+ type: 'warning',
+ text: i18n.ts.overwriteContentConfirm,
+ });
+
+ if (canceled) {
+ return;
+ }
+
+ pinnedEmojis.value = [...pinnedEmojisForReaction.value];
+}
+
+function remove(itemsRef: Ref<string[]>, reaction: string, ev: MouseEvent) {
+ os.popupMenu([{
+ text: i18n.ts.remove,
+ action: () => {
+ itemsRef.value = itemsRef.value.filter(x => x !== reaction);
+ },
+ }], getHTMLElement(ev));
+}
+
+async function setDefault(itemsRef: Ref<string[]>) {
+ const { canceled } = await os.confirm({
+ type: 'warning',
+ text: i18n.ts.resetAreYouSure,
+ });
+ if (canceled) return;
+
+ itemsRef.value = deepClone(defaultStore.def.reactions.default);
+}
+
+async function pickEmoji(itemsRef: Ref<string[]>, ev: MouseEvent) {
+ os.pickEmoji(getHTMLElement(ev), {
+ showPinned: false,
+ }).then(it => {
+ const emoji = it as string;
+ if (!itemsRef.value.includes(emoji)) {
+ itemsRef.value.push(emoji);
+ }
+ });
+}
+
+function getHTMLElement(ev: MouseEvent): HTMLElement {
+ const target = ev.currentTarget ?? ev.target;
+ return target as HTMLElement;
+}
+
+watch(pinnedEmojisForReaction, () => {
+ defaultStore.set('reactions', pinnedEmojisForReaction.value);
+}, {
+ deep: true,
+});
+
+watch(pinnedEmojis, () => {
+ defaultStore.set('pinnedEmojis', pinnedEmojis.value);
+}, {
+ deep: true,
+});
+
+definePageMetadata({
+ title: i18n.ts.emojiPicker,
+ icon: 'ti ti-mood-happy',
+});
+</script>
+
+<style lang="scss" module>
+.tab {
+ margin: calc(var(--margin) / 2) 0;
+ padding: calc(var(--margin) / 2) 0;
+ background: var(--bg);
+}
+
+.emojis {
+ padding: 12px;
+ font-size: 1.1em;
+}
+
+.emojisItem {
+ display: inline-block;
+ padding: 8px;
+ cursor: move;
+}
+
+.emojisAdd {
+ display: inline-block;
+ padding: 8px;
+}
+
+.editorCaption {
+ font-size: 0.85em;
+ padding: 8px 0 0 0;
+ color: var(--fgTransparentWeak);
+}
+</style>
diff --git a/packages/frontend/src/pages/settings/general.vue b/packages/frontend/src/pages/settings/general.vue
index 32dda149b8..0839a65ebb 100644
--- a/packages/frontend/src/pages/settings/general.vue
+++ b/packages/frontend/src/pages/settings/general.vue
@@ -66,6 +66,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<option value="sharkey"><i class="sk-icons sk-shark ph-bold" style="top: 2px;position: relative;"></i> Sharkey</option>
<option value="misskey"><i class="sk-icons sk-misskey ph-bold" style="top: 2px;position: relative;"></i> Misskey</option>
</MkRadios>
+ <MkSwitch v-model="limitWidthOfReaction">{{ i18n.ts.limitWidthOfReaction }}</MkSwitch>
</div>
<MkSelect v-model="instanceTicker">
@@ -136,7 +137,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkSwitch v-model="useSystemFont">{{ i18n.ts.useSystemFont }}</MkSwitch>
<MkSwitch v-model="disableDrawer">{{ i18n.ts.disableDrawer }}</MkSwitch>
<MkSwitch v-model="forceShowAds">{{ i18n.ts.forceShowAds }}</MkSwitch>
- <MkSwitch v-model="enableDataSaverMode">{{ i18n.ts.dataSaver }}</MkSwitch>
+ <MkSwitch v-model="enableSeasonalScreenEffect">{{ i18n.ts.seasonalScreenEffect }}</MkSwitch>
</div>
<div>
<MkRadios v-model="emojiStyle">
@@ -187,6 +188,37 @@ SPDX-License-Identifier: AGPL-3.0-only
<template #label>{{ i18n.ts.numberOfPageCache }}</template>
<template #caption>{{ i18n.ts.numberOfPageCacheDescription }}</template>
</MkRange>
+
+ <MkFolder>
+ <template #label>{{ i18n.ts.dataSaver }}</template>
+
+ <div class="_gaps_m">
+ <MkInfo>{{ i18n.ts.reloadRequiredToApplySettings }}</MkInfo>
+
+ <div class="_buttons">
+ <MkButton inline @click="enableAllDataSaver">{{ i18n.ts.enableAll }}</MkButton>
+ <MkButton inline @click="disableAllDataSaver">{{ i18n.ts.disableAll }}</MkButton>
+ </div>
+ <div class="_gaps_m">
+ <MkSwitch v-model="dataSaver.media">
+ {{ i18n.ts._dataSaver._media.title }}
+ <template #caption>{{ i18n.ts._dataSaver._media.description }}</template>
+ </MkSwitch>
+ <MkSwitch v-model="dataSaver.avatar">
+ {{ i18n.ts._dataSaver._avatar.title }}
+ <template #caption>{{ i18n.ts._dataSaver._avatar.description }}</template>
+ </MkSwitch>
+ <MkSwitch v-model="dataSaver.urlPreview">
+ {{ i18n.ts._dataSaver._urlPreview.title }}
+ <template #caption>{{ i18n.ts._dataSaver._urlPreview.description }}</template>
+ </MkSwitch>
+ <MkSwitch v-model="dataSaver.code">
+ {{ i18n.ts._dataSaver._code.title }}
+ <template #caption>{{ i18n.ts._dataSaver._code.description }}</template>
+ </MkSwitch>
+ </div>
+ </div>
+ </MkFolder>
</div>
</FormSection>
@@ -220,6 +252,7 @@ import MkButton from '@/components/MkButton.vue';
import FormSection from '@/components/form/section.vue';
import FormLink from '@/components/form/link.vue';
import MkLink from '@/components/MkLink.vue';
+import MkInfo from '@/components/MkInfo.vue';
import { langs } from '@/config.js';
import { defaultStore } from '@/store.js';
import * as os from '@/os.js';
@@ -234,6 +267,7 @@ const lang = ref(miLocalStorage.getItem('lang'));
const fontSize = ref(miLocalStorage.getItem('fontSize'));
const cornerRadius = ref(miLocalStorage.getItem('cornerRadius'));
const useSystemFont = ref(miLocalStorage.getItem('useSystemFont') != null);
+const dataSaver = ref(defaultStore.state.dataSaver);
async function reloadAsk() {
const { canceled } = await os.confirm({
@@ -250,6 +284,7 @@ const serverDisconnectedBehavior = computed(defaultStore.makeGetterSetter('serve
const showNoteActionsOnlyHover = computed(defaultStore.makeGetterSetter('showNoteActionsOnlyHover'));
const showClipButtonInNoteFooter = computed(defaultStore.makeGetterSetter('showClipButtonInNoteFooter'));
const reactionsDisplaySize = computed(defaultStore.makeGetterSetter('reactionsDisplaySize'));
+const limitWidthOfReaction = computed(defaultStore.makeGetterSetter('limitWidthOfReaction'));
const collapseRenotes = computed(defaultStore.makeGetterSetter('collapseRenotes'));
const clickToOpen = computed(defaultStore.makeGetterSetter('clickToOpen'));
const showBots = computed(defaultStore.makeGetterSetter('tlWithBots'));
@@ -267,7 +302,6 @@ const disableShowingAnimatedImages = computed(defaultStore.makeGetterSetter('dis
const forceShowAds = computed(defaultStore.makeGetterSetter('forceShowAds'));
const loadRawImages = computed(defaultStore.makeGetterSetter('loadRawImages'));
const highlightSensitiveMedia = computed(defaultStore.makeGetterSetter('highlightSensitiveMedia'));
-const enableDataSaverMode = computed(defaultStore.makeGetterSetter('enableDataSaverMode'));
const imageNewTab = computed(defaultStore.makeGetterSetter('imageNewTab'));
const nsfw = computed(defaultStore.makeGetterSetter('nsfw'));
const showFixedPostForm = computed(defaultStore.makeGetterSetter('showFixedPostForm'));
@@ -289,10 +323,12 @@ const showTickerOnReplies = computed(defaultStore.makeGetterSetter('showTickerOn
const noteDesign = computed(defaultStore.makeGetterSetter('noteDesign'));
const uncollapseCW = computed(defaultStore.makeGetterSetter('uncollapseCW'));
const expandLongNote = computed(defaultStore.makeGetterSetter('expandLongNote'));
+const enableSeasonalScreenEffect = computed(defaultStore.makeGetterSetter('enableSeasonalScreenEffect'));
watch(lang, () => {
miLocalStorage.setItem('lang', lang.value as string);
miLocalStorage.removeItem('locale');
+ miLocalStorage.removeItem('localeVersion');
});
watch(fontSize, () => {
@@ -338,9 +374,11 @@ watch([
overridedDeviceKind,
mediaListWithOneImageAppearance,
reactionsDisplaySize,
+ limitWidthOfReaction,
highlightSensitiveMedia,
keepScreenOn,
disableStreamingTimeline,
+ enableSeasonalScreenEffect,
], async () => {
await reloadAsk();
});
@@ -419,9 +457,31 @@ function testNotification(): void {
}, 300);
}
-const headerActions = $computed(() => []);
+function enableAllDataSaver() {
+ const g = { ...defaultStore.state.dataSaver };
+
+ Object.keys(g).forEach((key) => { g[key] = true; });
+
+ dataSaver.value = g;
+}
+
+function disableAllDataSaver() {
+ const g = { ...defaultStore.state.dataSaver };
+
+ Object.keys(g).forEach((key) => { g[key] = false; });
+
+ dataSaver.value = g;
+}
+
+watch(dataSaver, (to) => {
+ defaultStore.set('dataSaver', to);
+}, {
+ deep: true,
+});
+
+const headerActions = computed(() => []);
-const headerTabs = $computed(() => []);
+const headerTabs = computed(() => []);
definePageMetadata({
title: i18n.ts.general,
diff --git a/packages/frontend/src/pages/settings/import-export.vue b/packages/frontend/src/pages/settings/import-export.vue
index 31bfd7e732..7ca1faf406 100644
--- a/packages/frontend/src/pages/settings/import-export.vue
+++ b/packages/frontend/src/pages/settings/import-export.vue
@@ -126,7 +126,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { ref } from 'vue';
+import { ref, computed } from 'vue';
import MkButton from '@/components/MkButton.vue';
import FormSection from '@/components/form/section.vue';
import MkFolder from '@/components/MkFolder.vue';
@@ -233,9 +233,9 @@ const importAntennas = async (ev) => {
os.api('i/import-antennas', { fileId: file.id }).then(onImportSuccess).catch(onError);
};
-const headerActions = $computed(() => []);
+const headerActions = computed(() => []);
-const headerTabs = $computed(() => []);
+const headerTabs = computed(() => []);
definePageMetadata({
title: i18n.ts.importAndExport,
diff --git a/packages/frontend/src/pages/settings/index.vue b/packages/frontend/src/pages/settings/index.vue
index 04c1f2dbf2..558aed67a5 100644
--- a/packages/frontend/src/pages/settings/index.vue
+++ b/packages/frontend/src/pages/settings/index.vue
@@ -32,13 +32,11 @@ import { i18n } from '@/i18n.js';
import MkInfo from '@/components/MkInfo.vue';
import MkSuperMenu from '@/components/MkSuperMenu.vue';
import { signout, $i } from '@/account.js';
-import { unisonReload } from '@/scripts/unison-reload.js';
+import { clearCache } from '@/scripts/clear-cache.js';
import { instance } from '@/instance.js';
import { useRouter } from '@/router.js';
import { definePageMetadata, provideMetadataReceiver } from '@/scripts/page-metadata.js';
import * as os from '@/os.js';
-import { miLocalStorage } from '@/local-storage.js';
-import { fetchCustomEmojis } from '@/custom-emojis.js';
const indexInfo = {
title: i18n.ts.settings,
@@ -51,14 +49,14 @@ const childInfo = ref(null);
const router = useRouter();
-let narrow = $ref(false);
+const narrow = ref(false);
const NARROW_THRESHOLD = 600;
-let currentPage = $computed(() => router.currentRef.value.child);
+const currentPage = computed(() => router.currentRef.value.child);
const ro = new ResizeObserver((entries, observer) => {
if (entries.length === 0) return;
- narrow = entries[0].borderBoxSize[0].inlineSize < NARROW_THRESHOLD;
+ narrow.value = entries[0].borderBoxSize[0].inlineSize < NARROW_THRESHOLD;
});
const menuDef = computed(() => [{
@@ -67,37 +65,37 @@ const menuDef = computed(() => [{
icon: 'ph-user ph-bold ph-lg',
text: i18n.ts.profile,
to: '/settings/profile',
- active: currentPage?.route.name === 'profile',
+ active: currentPage.value?.route.name === 'profile',
}, {
icon: 'ph-lock ph-bold ph-lg-open',
text: i18n.ts.privacy,
to: '/settings/privacy',
- active: currentPage?.route.name === 'privacy',
+ active: currentPage.value?.route.name === 'privacy',
}, {
icon: 'ph-smiley ph-bold ph-lg',
- text: i18n.ts.reaction,
- to: '/settings/reaction',
- active: currentPage?.route.name === 'reaction',
+ text: i18n.ts.emojiPicker,
+ to: '/settings/emoji-picker',
+ active: currentPage.value?.route.name === 'emojiPicker',
}, {
icon: 'ph-cloud ph-bold ph-lg',
text: i18n.ts.drive,
to: '/settings/drive',
- active: currentPage?.route.name === 'drive',
+ active: currentPage.value?.route.name === 'drive',
}, {
icon: 'ph-bell ph-bold ph-lg',
text: i18n.ts.notifications,
to: '/settings/notifications',
- active: currentPage?.route.name === 'notifications',
+ active: currentPage.value?.route.name === 'notifications',
}, {
icon: 'ph-envelope ph-bold ph-lg',
text: i18n.ts.email,
to: '/settings/email',
- active: currentPage?.route.name === 'email',
+ active: currentPage.value?.route.name === 'email',
}, {
icon: 'ph-lock ph-bold ph-lg',
text: i18n.ts.security,
to: '/settings/security',
- active: currentPage?.route.name === 'security',
+ active: currentPage.value?.route.name === 'security',
}],
}, {
title: i18n.ts.clientSettings,
@@ -105,32 +103,32 @@ const menuDef = computed(() => [{
icon: 'ph-faders ph-bold ph-lg',
text: i18n.ts.general,
to: '/settings/general',
- active: currentPage?.route.name === 'general',
+ active: currentPage.value?.route.name === 'general',
}, {
icon: 'ph-palette ph-bold ph-lg',
text: i18n.ts.theme,
to: '/settings/theme',
- active: currentPage?.route.name === 'theme',
+ active: currentPage.value?.route.name === 'theme',
}, {
icon: 'ph-list ph-bold ph-lg-2',
text: i18n.ts.navbar,
to: '/settings/navbar',
- active: currentPage?.route.name === 'navbar',
+ active: currentPage.value?.route.name === 'navbar',
}, {
icon: 'ph-equals ph-bold ph-lg',
text: i18n.ts.statusbar,
to: '/settings/statusbar',
- active: currentPage?.route.name === 'statusbar',
+ active: currentPage.value?.route.name === 'statusbar',
}, {
icon: 'ph-music-notes ph-bold ph-lg',
text: i18n.ts.sounds,
to: '/settings/sounds',
- active: currentPage?.route.name === 'sounds',
+ active: currentPage.value?.route.name === 'sounds',
}, {
icon: 'ph-plug ph-bold ph-lg',
text: i18n.ts.plugins,
to: '/settings/plugin',
- active: currentPage?.route.name === 'plugin',
+ active: currentPage.value?.route.name === 'plugin',
}],
}, {
title: i18n.ts.otherSettings,
@@ -138,56 +136,50 @@ const menuDef = computed(() => [{
icon: 'ph-seal-check ph-bold ph-lg',
text: i18n.ts.roles,
to: '/settings/roles',
- active: currentPage?.route.name === 'roles',
+ active: currentPage.value?.route.name === 'roles',
}, {
icon: 'ph-prohibit ph-bold ph-lg',
text: i18n.ts.muteAndBlock,
to: '/settings/mute-block',
- active: currentPage?.route.name === 'mute-block',
+ active: currentPage.value?.route.name === 'mute-block',
}, {
icon: 'ph-key ph-bold ph-lg',
text: 'API',
to: '/settings/api',
- active: currentPage?.route.name === 'api',
+ active: currentPage.value?.route.name === 'api',
}, {
icon: 'ph-webhooks-logo ph-bold ph-lg',
text: 'Webhook',
to: '/settings/webhook',
- active: currentPage?.route.name === 'webhook',
+ active: currentPage.value?.route.name === 'webhook',
}, {
icon: 'ph-package ph-bold ph-lg',
text: i18n.ts.importAndExport,
to: '/settings/import-export',
- active: currentPage?.route.name === 'import-export',
+ active: currentPage.value?.route.name === 'import-export',
}, {
icon: 'ph-airplane ph-bold ph-lg',
text: `${i18n.ts.accountMigration}`,
to: '/settings/migration',
- active: currentPage?.route.name === 'migration',
+ active: currentPage.value?.route.name === 'migration',
}, {
icon: 'ph-dots-three ph-bold ph-lg',
text: i18n.ts.other,
to: '/settings/other',
- active: currentPage?.route.name === 'other',
+ active: currentPage.value?.route.name === 'other',
}],
}, {
items: [{
icon: 'ph-floppy-disk ph-bold ph-lg',
text: i18n.ts.preferencesBackups,
to: '/settings/preferences-backups',
- active: currentPage?.route.name === 'preferences-backups',
+ active: currentPage.value?.route.name === 'preferences-backups',
}, {
type: 'button',
icon: 'ph-trash ph-bold ph-lg',
text: i18n.ts.clearCache,
action: async () => {
- os.waiting();
- miLocalStorage.removeItem('locale');
- miLocalStorage.removeItem('theme');
- miLocalStorage.removeItem('emojis');
- miLocalStorage.removeItem('lastEmojisFetchedAt');
- await fetchCustomEmojis(true);
- unisonReload();
+ await clearCache();
},
}, {
type: 'button',
@@ -205,23 +197,23 @@ const menuDef = computed(() => [{
}],
}]);
-watch($$(narrow), () => {
+watch(narrow, () => {
});
onMounted(() => {
ro.observe(el.value);
- narrow = el.value.offsetWidth < NARROW_THRESHOLD;
+ narrow.value = el.value.offsetWidth < NARROW_THRESHOLD;
- if (!narrow && currentPage?.route.name == null) {
+ if (!narrow.value && currentPage.value?.route.name == null) {
router.replace('/settings/profile');
}
});
onActivated(() => {
- narrow = el.value.offsetWidth < NARROW_THRESHOLD;
+ narrow.value = el.value.offsetWidth < NARROW_THRESHOLD;
- if (!narrow && currentPage?.route.name == null) {
+ if (!narrow.value && currentPage.value?.route.name == null) {
router.replace('/settings/profile');
}
});
@@ -231,7 +223,7 @@ onUnmounted(() => {
});
watch(router.currentRef, (to) => {
- if (to.route.name === 'settings' && to.child?.route.name == null && !narrow) {
+ if (to.route.name === 'settings' && to.child?.route.name == null && !narrow.value) {
router.replace('/settings/profile');
}
});
@@ -243,12 +235,13 @@ provideMetadataReceiver((info) => {
childInfo.value = null;
} else {
childInfo.value = info;
+ INFO.value.needWideArea = info.value.needWideArea ?? undefined;
}
});
-const headerActions = $computed(() => []);
+const headerActions = computed(() => []);
-const headerTabs = $computed(() => []);
+const headerTabs = computed(() => []);
definePageMetadata(INFO);
// w 890
diff --git a/packages/frontend/src/pages/settings/mute-block.vue b/packages/frontend/src/pages/settings/mute-block.vue
index 32aa9bd754..6419d23a7a 100644
--- a/packages/frontend/src/pages/settings/mute-block.vue
+++ b/packages/frontend/src/pages/settings/mute-block.vue
@@ -9,7 +9,14 @@ SPDX-License-Identifier: AGPL-3.0-only
<template #icon><i class="ph-envelope ph-bold ph-lg"></i></template>
<template #label>{{ i18n.ts.wordMute }}</template>
- <XWordMute/>
+ <XWordMute :muted="$i!.mutedWords" @save="saveMutedWords"/>
+ </MkFolder>
+
+ <MkFolder>
+ <template #icon><i class="ti ti-message-off"></i></template>
+ <template #label>{{ i18n.ts.hardWordMute }}</template>
+
+ <XWordMute :muted="$i!.hardMutedWords" @save="saveHardMutedWords"/>
</MkFolder>
<MkFolder>
@@ -119,7 +126,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { } from 'vue';
+import { ref, computed } from 'vue';
import XInstanceMute from './mute-block.instance-mute.vue';
import XWordMute from './mute-block.word-mute.vue';
import MkPagination from '@/components/MkPagination.vue';
@@ -129,6 +136,7 @@ import { definePageMetadata } from '@/scripts/page-metadata.js';
import MkUserCardMini from '@/components/MkUserCardMini.vue';
import * as os from '@/os.js';
import { infoImageUrl } from '@/instance.js';
+import { $i } from '@/account.js';
import MkFolder from '@/components/MkFolder.vue';
const renoteMutingPagination = {
@@ -146,9 +154,9 @@ const blockingPagination = {
limit: 10,
};
-let expandedRenoteMuteItems = $ref([]);
-let expandedMuteItems = $ref([]);
-let expandedBlockItems = $ref([]);
+const expandedRenoteMuteItems = ref([]);
+const expandedMuteItems = ref([]);
+const expandedBlockItems = ref([]);
async function unrenoteMute(user, ev) {
os.popupMenu([{
@@ -184,32 +192,40 @@ async function unblock(user, ev) {
}
async function toggleRenoteMuteItem(item) {
- if (expandedRenoteMuteItems.includes(item.id)) {
- expandedRenoteMuteItems = expandedRenoteMuteItems.filter(x => x !== item.id);
+ if (expandedRenoteMuteItems.value.includes(item.id)) {
+ expandedRenoteMuteItems.value = expandedRenoteMuteItems.value.filter(x => x !== item.id);
} else {
- expandedRenoteMuteItems.push(item.id);
+ expandedRenoteMuteItems.value.push(item.id);
}
}
async function toggleMuteItem(item) {
- if (expandedMuteItems.includes(item.id)) {
- expandedMuteItems = expandedMuteItems.filter(x => x !== item.id);
+ if (expandedMuteItems.value.includes(item.id)) {
+ expandedMuteItems.value = expandedMuteItems.value.filter(x => x !== item.id);
} else {
- expandedMuteItems.push(item.id);
+ expandedMuteItems.value.push(item.id);
}
}
async function toggleBlockItem(item) {
- if (expandedBlockItems.includes(item.id)) {
- expandedBlockItems = expandedBlockItems.filter(x => x !== item.id);
+ if (expandedBlockItems.value.includes(item.id)) {
+ expandedBlockItems.value = expandedBlockItems.value.filter(x => x !== item.id);
} else {
- expandedBlockItems.push(item.id);
+ expandedBlockItems.value.push(item.id);
}
}
-const headerActions = $computed(() => []);
+async function saveMutedWords(mutedWords: (string | string[])[]) {
+ await os.api('i/update', { mutedWords });
+}
+
+async function saveHardMutedWords(hardMutedWords: (string | string[])[]) {
+ await os.api('i/update', { hardMutedWords });
+}
+
+const headerActions = computed(() => []);
-const headerTabs = $computed(() => []);
+const headerTabs = computed(() => []);
definePageMetadata({
title: i18n.ts.muteAndBlock,
diff --git a/packages/frontend/src/pages/settings/mute-block.word-mute.vue b/packages/frontend/src/pages/settings/mute-block.word-mute.vue
index 4b6d27d8f2..96ee48cdba 100644
--- a/packages/frontend/src/pages/settings/mute-block.word-mute.vue
+++ b/packages/frontend/src/pages/settings/mute-block.word-mute.vue
@@ -18,16 +18,17 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { ref, watch } from 'vue';
import MkTextarea from '@/components/MkTextarea.vue';
-import MkKeyValue from '@/components/MkKeyValue.vue';
import MkButton from '@/components/MkButton.vue';
-import MkInfo from '@/components/MkInfo.vue';
-import MkTab from '@/components/MkTab.vue';
import * as os from '@/os.js';
-import number from '@/filters/number.js';
-import { defaultStore } from '@/store.js';
-import { $i } from '@/account.js';
import { i18n } from '@/i18n.js';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
+
+const props = defineProps<{
+ muted: (string[] | string)[];
+}>();
+
+const emit = defineEmits<{
+ (ev: 'save', value: (string[] | string)[]): void;
+}>();
const render = (mutedWords) => mutedWords.map(x => {
if (Array.isArray(x)) {
@@ -37,8 +38,7 @@ const render = (mutedWords) => mutedWords.map(x => {
}
}).join('\n');
-const tab = ref('soft');
-const mutedWords = ref(render($i!.mutedWords));
+const mutedWords = ref(render(props.muted));
const changed = ref(false);
watch(mutedWords, () => {
@@ -85,9 +85,7 @@ async function save() {
return;
}
- await os.api('i/update', {
- mutedWords: parsed,
- });
+ emit('save', parsed);
changed.value = false;
}
diff --git a/packages/frontend/src/pages/settings/navbar.vue b/packages/frontend/src/pages/settings/navbar.vue
index 51a826ef3e..0112543781 100644
--- a/packages/frontend/src/pages/settings/navbar.vue
+++ b/packages/frontend/src/pages/settings/navbar.vue
@@ -57,7 +57,6 @@ import { defaultStore } from '@/store.js';
import { unisonReload } from '@/scripts/unison-reload.js';
import { i18n } from '@/i18n.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';
-import { deepClone } from '@/scripts/clone.js';
const Sortable = defineAsyncComponent(() => import('vuedraggable').then(x => x.default));
@@ -115,9 +114,9 @@ watch(menuDisplay, async () => {
await reloadAsk();
});
-const headerActions = $computed(() => []);
+const headerActions = computed(() => []);
-const headerTabs = $computed(() => []);
+const headerTabs = computed(() => []);
definePageMetadata({
title: i18n.ts.navbar,
diff --git a/packages/frontend/src/pages/settings/notifications.notification-config.vue b/packages/frontend/src/pages/settings/notifications.notification-config.vue
index 202b018efd..06686c3204 100644
--- a/packages/frontend/src/pages/settings/notifications.notification-config.vue
+++ b/packages/frontend/src/pages/settings/notifications.notification-config.vue
@@ -26,7 +26,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { } from 'vue';
+import { ref } from 'vue';
import * as Misskey from 'misskey-js';
import MkSelect from '@/components/MkSelect.vue';
import MkButton from '@/components/MkButton.vue';
@@ -41,10 +41,10 @@ const emit = defineEmits<{
(ev: 'update', result: any): void;
}>();
-let type = $ref(props.value.type);
-let userListId = $ref(props.value.userListId);
+const type = ref(props.value.type);
+const userListId = ref(props.value.userListId);
function save() {
- emit('update', { type, userListId });
+ emit('update', { type: type.value, userListId: userListId.value });
}
</script>
diff --git a/packages/frontend/src/pages/settings/notifications.vue b/packages/frontend/src/pages/settings/notifications.vue
index e5a3ddf88b..0bdfbdf741 100644
--- a/packages/frontend/src/pages/settings/notifications.vue
+++ b/packages/frontend/src/pages/settings/notifications.vue
@@ -55,7 +55,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { defineAsyncComponent } from 'vue';
+import { shallowRef, computed } from 'vue';
import XNotificationConfig from './notifications.notification-config.vue';
import FormLink from '@/components/form/link.vue';
import FormSection from '@/components/form/section.vue';
@@ -68,11 +68,11 @@ import { definePageMetadata } from '@/scripts/page-metadata.js';
import MkPushNotificationAllowButton from '@/components/MkPushNotificationAllowButton.vue';
import { notificationTypes } from '@/const.js';
-const nonConfigurableNotificationTypes = ['note'];
+const nonConfigurableNotificationTypes = ['note', 'roleAssigned', 'followRequestAccepted', 'achievementEarned'];
-let allowButton = $shallowRef<InstanceType<typeof MkPushNotificationAllowButton>>();
-let pushRegistrationInServer = $computed(() => allowButton?.pushRegistrationInServer);
-let sendReadMessage = $computed(() => pushRegistrationInServer?.sendReadMessage || false);
+const allowButton = shallowRef<InstanceType<typeof MkPushNotificationAllowButton>>();
+const pushRegistrationInServer = computed(() => allowButton.value?.pushRegistrationInServer);
+const sendReadMessage = computed(() => pushRegistrationInServer.value?.sendReadMessage || false);
const userLists = await os.api('users/lists/list');
async function readAllUnreadNotes() {
@@ -95,14 +95,14 @@ async function updateReceiveConfig(type, value) {
}
function onChangeSendReadMessage(v: boolean) {
- if (!pushRegistrationInServer) return;
+ if (!pushRegistrationInServer.value) return;
os.apiWithDialog('sw/update-registration', {
- endpoint: pushRegistrationInServer.endpoint,
+ endpoint: pushRegistrationInServer.value.endpoint,
sendReadMessage: v,
}).then(res => {
- if (!allowButton) return;
- allowButton.pushRegistrationInServer = res;
+ if (!allowButton.value) return;
+ allowButton.value.pushRegistrationInServer = res;
});
}
@@ -110,9 +110,9 @@ function testNotification(): void {
os.api('notifications/test-notification');
}
-const headerActions = $computed(() => []);
+const headerActions = computed(() => []);
-const headerTabs = $computed(() => []);
+const headerTabs = computed(() => []);
definePageMetadata({
title: i18n.ts.notifications,
diff --git a/packages/frontend/src/pages/settings/other.vue b/packages/frontend/src/pages/settings/other.vue
index 758e41047d..efda0c00b3 100644
--- a/packages/frontend/src/pages/settings/other.vue
+++ b/packages/frontend/src/pages/settings/other.vue
@@ -188,9 +188,9 @@ watch([
await reloadAsk();
});
-const headerActions = $computed(() => []);
+const headerActions = computed(() => []);
-const headerTabs = $computed(() => []);
+const headerTabs = computed(() => []);
definePageMetadata({
title: i18n.ts.other,
diff --git a/packages/frontend/src/pages/settings/plugin.install.vue b/packages/frontend/src/pages/settings/plugin.install.vue
index 1546223f12..be8db548a4 100644
--- a/packages/frontend/src/pages/settings/plugin.install.vue
+++ b/packages/frontend/src/pages/settings/plugin.install.vue
@@ -7,9 +7,9 @@ SPDX-License-Identifier: AGPL-3.0-only
<div class="_gaps_m">
<FormInfo warn>{{ i18n.ts._plugin.installWarn }}</FormInfo>
- <MkTextarea v-model="code" tall>
+ <MkCodeEditor v-model="code" lang="is">
<template #label>{{ i18n.ts.code }}</template>
- </MkTextarea>
+ </MkCodeEditor>
<div>
<MkButton :disabled="code == null" primary inline @click="install"><i class="ph-check ph-bold ph-lg"></i> {{ i18n.ts.install }}</MkButton>
@@ -18,8 +18,8 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { nextTick, ref } from 'vue';
-import MkTextarea from '@/components/MkTextarea.vue';
+import { nextTick, ref, computed } from 'vue';
+import MkCodeEditor from '@/components/MkCodeEditor.vue';
import MkButton from '@/components/MkButton.vue';
import FormInfo from '@/components/MkInfo.vue';
import * as os from '@/os.js';
@@ -49,9 +49,9 @@ async function install() {
}
}
-const headerActions = $computed(() => []);
+const headerActions = computed(() => []);
-const headerTabs = $computed(() => []);
+const headerTabs = computed(() => []);
definePageMetadata({
title: i18n.ts._plugin.install,
diff --git a/packages/frontend/src/pages/settings/plugin.vue b/packages/frontend/src/pages/settings/plugin.vue
index 7447cef431..5b5c282f39 100644
--- a/packages/frontend/src/pages/settings/plugin.vue
+++ b/packages/frontend/src/pages/settings/plugin.vue
@@ -60,7 +60,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { nextTick, ref } from 'vue';
+import { nextTick, ref, computed } from 'vue';
import FormLink from '@/components/form/link.vue';
import MkSwitch from '@/components/MkSwitch.vue';
import FormSection from '@/components/form/section.vue';
@@ -121,9 +121,9 @@ function changeActive(plugin, active) {
});
}
-const headerActions = $computed(() => []);
+const headerActions = computed(() => []);
-const headerTabs = $computed(() => []);
+const headerTabs = computed(() => []);
definePageMetadata({
title: i18n.ts.plugins,
diff --git a/packages/frontend/src/pages/settings/preferences-backups.vue b/packages/frontend/src/pages/settings/preferences-backups.vue
index f7768ebe5c..c7538f3a1b 100644
--- a/packages/frontend/src/pages/settings/preferences-backups.vue
+++ b/packages/frontend/src/pages/settings/preferences-backups.vue
@@ -54,22 +54,24 @@ import { miLocalStorage } from '@/local-storage.js';
const { t, ts } = i18n;
const defaultStoreSaveKeys: (keyof typeof defaultStore['state'])[] = [
+ 'collapseRenotes',
'menu',
'visibility',
'localOnly',
'statusbars',
'widgets',
'tl',
+ 'pinnedUserLists',
'overridedDeviceKind',
'serverDisconnectedBehavior',
- 'collapseRenotes',
- 'showNoteActionsOnlyHover',
'nsfw',
+ 'highlightSensitiveMedia',
'animation',
'animatedMfm',
'advancedMfm',
'loadRawImages',
'imageNewTab',
+ 'dataSaver',
'disableShowingAnimatedImages',
'emojiStyle',
'disableDrawer',
@@ -81,18 +83,37 @@ const defaultStoreSaveKeys: (keyof typeof defaultStore['state'])[] = [
'useReactionPickerForContextMenu',
'showGapBetweenNotesInTimeline',
'instanceTicker',
- 'reactionPickerSize',
- 'reactionPickerWidth',
- 'reactionPickerHeight',
- 'reactionPickerUseDrawerForMobile',
+ 'emojiPickerScale',
+ 'emojiPickerWidth',
+ 'emojiPickerHeight',
+ 'emojiPickerUseDrawerForMobile',
'defaultSideView',
'menuDisplay',
'reportError',
'squareAvatars',
+ 'showAvatarDecorations',
'numberOfPageCache',
+ 'showNoteActionsOnlyHover',
+ 'showClipButtonInNoteFooter',
+ 'reactionsDisplaySize',
+ 'forceShowAds',
'numberOfReplies',
'aiChanMode',
+ 'devMode',
'mediaListWithOneImageAppearance',
+ 'notificationPosition',
+ 'notificationStackAxis',
+ 'enableCondensedLineForAcct',
+ 'keepScreenOn',
+ 'defaultWithReplies',
+ 'disableStreamingTimeline',
+ 'useGroupedNotifications',
+ 'sound_masterVolume',
+ 'sound_note',
+ 'sound_noteMy',
+ 'sound_notification',
+ 'sound_antenna',
+ 'sound_channel',
];
const coldDeviceStorageSaveKeys: (keyof typeof ColdDeviceStorage.default)[] = [
'lightTheme',
@@ -395,7 +416,7 @@ function menu(ev: MouseEvent, profileId: string) {
icon: 'ph-download ph-bold ph-lg',
href: URL.createObjectURL(new Blob([JSON.stringify(profiles.value[profileId], null, 2)], { type: 'application/json' })),
download: `${profiles.value[profileId].name}.json`,
- }, null, {
+ }, { type: 'divider' }, {
text: ts.rename,
icon: 'ph-textbox ph-bold ph-lg',
action: () => rename(profileId),
@@ -403,7 +424,7 @@ function menu(ev: MouseEvent, profileId: string) {
text: ts._preferencesBackups.save,
icon: 'ph-floppy-disk ph-bold ph-lg',
action: () => save(profileId),
- }, null, {
+ }, { type: 'divider' }, {
text: ts.delete,
icon: 'ph-trash ph-bold ph-lg',
action: () => deleteProfile(profileId),
diff --git a/packages/frontend/src/pages/settings/privacy.vue b/packages/frontend/src/pages/settings/privacy.vue
index dae4251892..62056ff8a6 100644
--- a/packages/frontend/src/pages/settings/privacy.vue
+++ b/packages/frontend/src/pages/settings/privacy.vue
@@ -13,12 +13,18 @@ SPDX-License-Identifier: AGPL-3.0-only
<template #caption>{{ i18n.ts.makeReactionsPublicDescription }}</template>
</MkSwitch>
- <MkSelect v-model="ffVisibility" @update:modelValue="save()">
- <template #label>{{ i18n.ts.ffVisibility }}</template>
+ <MkSelect v-model="followingVisibility" @update:modelValue="save()">
+ <template #label>{{ i18n.ts.followingVisibility }}</template>
+ <option value="public">{{ i18n.ts._ffVisibility.public }}</option>
+ <option value="followers">{{ i18n.ts._ffVisibility.followers }}</option>
+ <option value="private">{{ i18n.ts._ffVisibility.private }}</option>
+ </MkSelect>
+
+ <MkSelect v-model="followersVisibility" @update:modelValue="save()">
+ <template #label>{{ i18n.ts.followersVisibility }}</template>
<option value="public">{{ i18n.ts._ffVisibility.public }}</option>
<option value="followers">{{ i18n.ts._ffVisibility.followers }}</option>
<option value="private">{{ i18n.ts._ffVisibility.private }}</option>
- <template #caption>{{ i18n.ts.ffVisibilityDescription }}</template>
</MkSelect>
<MkSwitch v-model="hideOnlineStatus" @update:modelValue="save()">
@@ -66,7 +72,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { } from 'vue';
+import { ref, computed } from 'vue';
import MkSwitch from '@/components/MkSwitch.vue';
import MkSelect from '@/components/MkSelect.vue';
import FormSection from '@/components/form/section.vue';
@@ -77,36 +83,38 @@ import { i18n } from '@/i18n.js';
import { $i } from '@/account.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';
-let isLocked = $ref($i.isLocked);
-let autoAcceptFollowed = $ref($i.autoAcceptFollowed);
-let noCrawle = $ref($i.noCrawle);
-let isExplorable = $ref($i.isExplorable);
-let noindex = $ref($i.noindex);
-let hideOnlineStatus = $ref($i.hideOnlineStatus);
-let publicReactions = $ref($i.publicReactions);
-let ffVisibility = $ref($i.ffVisibility);
+const isLocked = ref($i.isLocked);
+const autoAcceptFollowed = ref($i.autoAcceptFollowed);
+const noCrawle = ref($i.noCrawle);
+const noindex = ref($i.noindex);
+const isExplorable = ref($i.isExplorable);
+const hideOnlineStatus = ref($i.hideOnlineStatus);
+const publicReactions = ref($i.publicReactions);
+const followingVisibility = ref($i?.followingVisibility);
+const followersVisibility = ref($i?.followersVisibility);
-let defaultNoteVisibility = $computed(defaultStore.makeGetterSetter('defaultNoteVisibility'));
-let defaultNoteLocalOnly = $computed(defaultStore.makeGetterSetter('defaultNoteLocalOnly'));
-let rememberNoteVisibility = $computed(defaultStore.makeGetterSetter('rememberNoteVisibility'));
-let keepCw = $computed(defaultStore.makeGetterSetter('keepCw'));
+const defaultNoteVisibility = computed(defaultStore.makeGetterSetter('defaultNoteVisibility'));
+const defaultNoteLocalOnly = computed(defaultStore.makeGetterSetter('defaultNoteLocalOnly'));
+const rememberNoteVisibility = computed(defaultStore.makeGetterSetter('rememberNoteVisibility'));
+const keepCw = computed(defaultStore.makeGetterSetter('keepCw'));
function save() {
os.api('i/update', {
- isLocked: !!isLocked,
- autoAcceptFollowed: !!autoAcceptFollowed,
- noCrawle: !!noCrawle,
- isExplorable: !!isExplorable,
- noindex: !!noindex,
- hideOnlineStatus: !!hideOnlineStatus,
- publicReactions: !!publicReactions,
- ffVisibility: ffVisibility,
+ isLocked: !!isLocked.value,
+ autoAcceptFollowed: !!autoAcceptFollowed.value,
+ noCrawle: !!noCrawle.value,
+ noindex: !!noindex.value,
+ isExplorable: !!isExplorable.value,
+ hideOnlineStatus: !!hideOnlineStatus.value,
+ publicReactions: !!publicReactions.value,
+ followingVisibility: followingVisibility.value,
+ followersVisibility: followersVisibility.value,
});
}
-const headerActions = $computed(() => []);
+const headerActions = computed(() => []);
-const headerTabs = $computed(() => []);
+const headerTabs = computed(() => []);
definePageMetadata({
title: i18n.ts.privacy,
diff --git a/packages/frontend/src/pages/settings/profile.avatar-decoration-dialog.vue b/packages/frontend/src/pages/settings/profile.avatar-decoration-dialog.vue
deleted file mode 100644
index d0c50d59cd..0000000000
--- a/packages/frontend/src/pages/settings/profile.avatar-decoration-dialog.vue
+++ /dev/null
@@ -1,114 +0,0 @@
-<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
-SPDX-License-Identifier: AGPL-3.0-only
--->
-
-<template>
-<MkModalWindow
- ref="dialog"
- :width="400"
- :height="450"
- @close="cancel"
- @closed="emit('closed')"
->
- <template #header>{{ i18n.ts.avatarDecorations }}</template>
-
- <div>
- <MkSpacer :marginMin="20" :marginMax="28">
- <div style="text-align: center;">
- <div :class="$style.name">{{ decoration.name }}</div>
- <MkAvatar style="width: 64px; height: 64px; margin-bottom: 20px;" :user="$i" :decoration="{ url: decoration.url, angle, flipH }" forceShowDecoration/>
- </div>
- <div class="_gaps_s">
- <MkRange v-model="angle" continuousUpdate :min="-0.5" :max="0.5" :step="0.025" :textConverter="(v) => `${Math.floor(v * 360)}°`">
- <template #label>{{ i18n.ts.angle }}</template>
- </MkRange>
- <MkSwitch v-model="flipH">
- <template #label>{{ i18n.ts.flip }}</template>
- </MkSwitch>
- </div>
- </MkSpacer>
-
- <div :class="$style.footer" class="_buttonsCenter">
- <MkButton v-if="using" primary rounded @click="attach"><i class="ph-check ph-bold ph-lg"></i> {{ i18n.ts.update }}</MkButton>
- <MkButton v-if="using" rounded @click="detach"><i class="ph-x ph-bold ph-lg"></i> {{ i18n.ts.detach }}</MkButton>
- <MkButton v-else primary rounded @click="attach"><i class="ph-check ph-bold ph-lg"></i> {{ i18n.ts.attach }}</MkButton>
- </div>
- </div>
-</MkModalWindow>
-</template>
-
-<script lang="ts" setup>
-import { shallowRef, ref, computed } from 'vue';
-import MkButton from '@/components/MkButton.vue';
-import MkModalWindow from '@/components/MkModalWindow.vue';
-import MkSwitch from '@/components/MkSwitch.vue';
-import { i18n } from '@/i18n.js';
-import * as os from '@/os.js';
-import MkFolder from '@/components/MkFolder.vue';
-import MkInfo from '@/components/MkInfo.vue';
-import MkRange from '@/components/MkRange.vue';
-import { $i } from '@/account.js';
-
-const props = defineProps<{
- decoration: {
- id: string;
- url: string;
- }
-}>();
-
-const emit = defineEmits<{
- (ev: 'closed'): void;
-}>();
-
-const dialog = shallowRef<InstanceType<typeof MkModalWindow>>();
-const using = computed(() => $i.avatarDecorations.some(x => x.id === props.decoration.id));
-const angle = ref(using.value ? $i.avatarDecorations.find(x => x.id === props.decoration.id).angle ?? 0 : 0);
-const flipH = ref(using.value ? $i.avatarDecorations.find(x => x.id === props.decoration.id).flipH ?? false : false);
-
-function cancel() {
- dialog.value.close();
-}
-
-async function attach() {
- const decoration = {
- id: props.decoration.id,
- angle: angle.value,
- flipH: flipH.value,
- };
- await os.apiWithDialog('i/update', {
- avatarDecorations: [decoration],
- });
- $i.avatarDecorations = [decoration];
-
- dialog.value.close();
-}
-
-async function detach() {
- await os.apiWithDialog('i/update', {
- avatarDecorations: [],
- });
- $i.avatarDecorations = [];
-
- dialog.value.close();
-}
-</script>
-
-<style lang="scss" module>
-.name {
- position: relative;
- z-index: 10;
- font-weight: bold;
- margin-bottom: 28px;
-}
-
-.footer {
- position: sticky;
- bottom: 0;
- left: 0;
- padding: 12px;
- border-top: solid 0.5px var(--divider);
- -webkit-backdrop-filter: var(--blur, blur(15px));
- backdrop-filter: var(--blur, blur(15px));
-}
-</style>
diff --git a/packages/frontend/src/pages/settings/profile.vue b/packages/frontend/src/pages/settings/profile.vue
index b585333bbd..4bae635d05 100644
--- a/packages/frontend/src/pages/settings/profile.vue
+++ b/packages/frontend/src/pages/settings/profile.vue
@@ -5,20 +5,25 @@ SPDX-License-Identifier: AGPL-3.0-only
<template>
<div class="_gaps_m">
- <div :class="$style.avatarAndBanner" :style="{ backgroundImage: $i.bannerUrl ? `url(${ $i.bannerUrl })` : null }">
+ <div class="_panel">
+ <div :class="$style.banner" :style="{ backgroundImage: $i.bannerUrl ? `url(${ $i.bannerUrl })` : null }">
+ <MkButton primary rounded :class="$style.bannerEdit" @click="changeBanner">{{ i18n.ts._profile.changeBanner }}</MkButton>
+ <MkButton primary rounded :class="$style.backgroundEdit" @click="changeBackground">{{ i18n.ts._profile.changeBackground }}</MkButton>
+ </div>
<div :class="$style.avatarContainer">
<MkAvatar :class="$style.avatar" :user="$i" forceShowDecoration @click="changeAvatar"/>
- <MkButton primary rounded @click="changeAvatar">{{ i18n.ts._profile.changeAvatar }}</MkButton>
+ <div class="_buttonsCenter">
+ <MkButton primary rounded @click="changeAvatar">{{ i18n.ts._profile.changeAvatar }}</MkButton>
+ <MkButton primary rounded link to="/settings/avatar-decoration">{{ i18n.ts.decorate }} <i class="ph-sparkle ph-bold ph-lg"></i></MkButton>
+ </div>
</div>
- <MkButton primary rounded :class="$style.backgroundEdit" @click="changeBackground">{{ i18n.ts._profile.changeBackground }}</MkButton>
- <MkButton primary rounded :class="$style.bannerEdit" @click="changeBanner">{{ i18n.ts._profile.changeBanner }}</MkButton>
</div>
- <MkInput v-model="profile.name" :max="30" manualSave>
+ <MkInput v-model="profile.name" :max="30" manualSave :mfmAutocomplete="['emoji']">
<template #label>{{ i18n.ts._profile.name }}</template>
</MkInput>
- <MkTextarea v-model="profile.description" :max="500" tall manualSave>
+ <MkTextarea v-model="profile.description" :max="500" tall manualSave mfmAutocomplete :mfmPreview="true">
<template #label>{{ i18n.ts._profile.description }}</template>
<template #caption>{{ i18n.ts._profile.youCanIncludeHashtags }}</template>
</MkTextarea>
@@ -90,24 +95,6 @@ SPDX-License-Identifier: AGPL-3.0-only
</FormSlot>
<MkFolder>
- <template #icon><i class="ph-sparkle ph-bold ph-lg"></i></template>
- <template #label>{{ i18n.ts.avatarDecorations }}</template>
-
- <div style="display: grid; grid-template-columns: repeat(auto-fill, minmax(140px, 1fr)); grid-gap: 12px;">
- <div
- v-for="avatarDecoration in avatarDecorations"
- :key="avatarDecoration.id"
- :class="[$style.avatarDecoration, { [$style.avatarDecorationActive]: $i.avatarDecorations.some(x => x.id === avatarDecoration.id) }]"
- @click="openDecoration(avatarDecoration)"
- >
- <div :class="$style.avatarDecorationName"><MkCondensedLine :minScale="0.5">{{ avatarDecoration.name }}</MkCondensedLine></div>
- <MkAvatar style="width: 60px; height: 60px;" :user="$i" :decoration="{ url: avatarDecoration.url }" forceShowDecoration/>
- <i v-if="avatarDecoration.roleIdsThatCanBeUsedThisDecoration.length > 0 && !$i.roles.some(r => avatarDecoration.roleIdsThatCanBeUsedThisDecoration.includes(r.id))" :class="$style.avatarDecorationLock" class="ph-lock ph-bold ph-lg"></i>
- </div>
- </div>
- </MkFolder>
-
- <MkFolder>
<template #label>{{ i18n.ts.advancedSettings }}</template>
<div class="_gaps_m">
@@ -129,10 +116,9 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { computed, reactive, ref, watch, defineAsyncComponent, onMounted, onUnmounted } from 'vue';
+import { computed, reactive, ref, watch, defineAsyncComponent } from 'vue';
import MkButton from '@/components/MkButton.vue';
import MkInput from '@/components/MkInput.vue';
-import MkTextarea from '@/components/MkTextarea.vue';
import MkSwitch from '@/components/MkSwitch.vue';
import MkSelect from '@/components/MkSelect.vue';
import FormSplit from '@/components/form/split.vue';
@@ -147,11 +133,11 @@ import { definePageMetadata } from '@/scripts/page-metadata.js';
import { claimAchievement } from '@/scripts/achievements.js';
import { defaultStore } from '@/store.js';
import MkInfo from '@/components/MkInfo.vue';
+import MkTextarea from '@/components/MkTextarea.vue';
const Sortable = defineAsyncComponent(() => import('vuedraggable').then(x => x.default));
const reactionAcceptance = computed(defaultStore.makeGetterSetter('reactionAcceptance'));
-let avatarDecorations: any[] = $ref([]);
const now = new Date();
@@ -182,10 +168,6 @@ watch(() => profile, () => {
const fields = ref($i?.fields.map(field => ({ id: Math.random().toString(), name: field.name, value: field.value })) ?? []);
const fieldEditMode = ref(false);
-os.api('get-avatar-decorations').then(_avatarDecorations => {
- avatarDecorations = _avatarDecorations;
-});
-
function addField() {
fields.value.push({
id: Math.random().toString(),
@@ -318,15 +300,9 @@ function changeBackground(ev) {
});
}
-function openDecoration(avatarDecoration) {
- os.popup(defineAsyncComponent(() => import('./profile.avatar-decoration-dialog.vue')), {
- decoration: avatarDecoration,
- }, {}, 'closed');
-}
-
-const headerActions = $computed(() => []);
+const headerActions = computed(() => []);
-const headerTabs = $computed(() => []);
+const headerTabs = computed(() => []);
definePageMetadata({
title: i18n.ts.profile,
@@ -335,19 +311,19 @@ definePageMetadata({
</script>
<style lang="scss" module>
-.avatarAndBanner {
+.banner {
position: relative;
+ height: 130px;
background-size: cover;
background-position: center;
- border: solid 1px var(--divider);
- border-radius: var(--radius);
+ border-bottom: solid 1px var(--divider);
overflow: clip;
}
.avatarContainer {
- display: inline-block;
+ margin-top: -50px;
+ padding-bottom: 16px;
text-align: center;
- padding: 16px;
}
.avatar {
@@ -364,7 +340,7 @@ definePageMetadata({
}
.backgroundEdit {
position: absolute;
- top: 103px;
+ top: 95px;
right: 16px;
}
@@ -423,33 +399,4 @@ definePageMetadata({
.dragItemForm {
flex-grow: 1;
}
-
-.avatarDecoration {
- cursor: pointer;
- padding: 16px 16px 28px 16px;
- border: solid 2px var(--divider);
- border-radius: var(--radius-sm);
- text-align: center;
- font-size: 90%;
- overflow: clip;
- contain: content;
-}
-
-.avatarDecorationActive {
- background-color: var(--accentedBg);
- border-color: var(--accent);
-}
-
-.avatarDecorationName {
- position: relative;
- z-index: 10;
- font-weight: bold;
- margin-bottom: 20px;
-}
-
-.avatarDecorationLock {
- position: absolute;
- bottom: 12px;
- right: 12px;
-}
</style>
diff --git a/packages/frontend/src/pages/settings/reaction.vue b/packages/frontend/src/pages/settings/reaction.vue
deleted file mode 100644
index 213e73b526..0000000000
--- a/packages/frontend/src/pages/settings/reaction.vue
+++ /dev/null
@@ -1,196 +0,0 @@
-<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
-SPDX-License-Identifier: AGPL-3.0-only
--->
-
-<template>
-<div class="_gaps_m">
- <FromSlot>
- <template #label>{{ i18n.ts.reactionSettingDescription }}</template>
- <div v-panel style="border-radius: var(--radius-sm);">
- <Sortable v-model="reactions" :class="$style.reactions" :itemKey="item => item" :animation="150" :delay="100" :delayOnTouchOnly="true">
- <template #item="{element}">
- <button class="_button" :class="$style.reactionsItem" @click="remove(element, $event)">
- <MkCustomEmoji v-if="element[0] === ':'" :name="element" :normal="true"/>
- <MkEmoji v-else :emoji="element" :normal="true"/>
- </button>
- </template>
- <template #footer>
- <button class="_button" :class="$style.reactionsAdd" @click="chooseEmoji"><i class="ph-plus ph-bold ph-lg"></i></button>
- </template>
- </Sortable>
- </div>
- <template #caption>{{ i18n.ts.reactionSettingDescription2 }} <button class="_textButton" @click="preview">{{ i18n.ts.preview }}</button></template>
- </FromSlot>
-
- <FromSlot>
- <template #label>{{ i18n.ts.defaultLike }}</template>
- <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-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;">
- <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>
- </div>
- </FromSlot>
-
- <MkRadios v-model="reactionPickerSize">
- <template #label>{{ i18n.ts.size }}</template>
- <option :value="1">{{ i18n.ts.small }}</option>
- <option :value="2">{{ i18n.ts.medium }}</option>
- <option :value="3">{{ i18n.ts.large }}</option>
- </MkRadios>
- <MkRadios v-model="reactionPickerWidth">
- <template #label>{{ i18n.ts.numberOfColumn }}</template>
- <option :value="1">5</option>
- <option :value="2">6</option>
- <option :value="3">7</option>
- <option :value="4">8</option>
- <option :value="5">9</option>
- </MkRadios>
- <MkRadios v-model="reactionPickerHeight">
- <template #label>{{ i18n.ts.height }}</template>
- <option :value="1">{{ i18n.ts.small }}</option>
- <option :value="2">{{ i18n.ts.medium }}</option>
- <option :value="3">{{ i18n.ts.large }}</option>
- <option :value="4">{{ i18n.ts.large }}+</option>
- </MkRadios>
-
- <MkSwitch v-model="reactionPickerUseDrawerForMobile">
- {{ i18n.ts.useDrawerReactionPickerForMobile }}
- <template #caption>{{ i18n.ts.needReloadToApply }}</template>
- </MkSwitch>
-
- <FormSection>
- <div class="_buttons">
- <MkButton inline @click="preview"><i class="ph-eye ph-bold ph-lg"></i> {{ i18n.ts.preview }}</MkButton>
- <MkButton inline danger @click="setDefault"><i class="ph-arrow-clockwise ph-bold ph-lg"></i> {{ i18n.ts.default }}</MkButton>
- </div>
- </FormSection>
-</div>
-</template>
-
-<script lang="ts" setup>
-import { defineAsyncComponent, watch } from 'vue';
-import Sortable from 'vuedraggable';
-import MkRadios from '@/components/MkRadios.vue';
-import FromSlot from '@/components/form/slot.vue';
-import MkButton from '@/components/MkButton.vue';
-import FormSection from '@/components/form/section.vue';
-import MkSwitch from '@/components/MkSwitch.vue';
-import * as os from '@/os.js';
-import { defaultStore } from '@/store.js';
-import { i18n } from '@/i18n.js';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
-import { deepClone } from '@/scripts/clone.js';
-import { unisonReload } from '@/scripts/unison-reload.js';
-
-let reactions = $ref(deepClone(defaultStore.state.reactions));
-const like = $computed(defaultStore.makeGetterSetter('like'));
-
-const reactionPickerSize = $computed(defaultStore.makeGetterSetter('reactionPickerSize'));
-const reactionPickerWidth = $computed(defaultStore.makeGetterSetter('reactionPickerWidth'));
-const reactionPickerHeight = $computed(defaultStore.makeGetterSetter('reactionPickerHeight'));
-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() {
- defaultStore.set('reactions', reactions);
-}
-
-function remove(reaction, ev: MouseEvent) {
- os.popupMenu([{
- text: i18n.ts.remove,
- action: () => {
- reactions = reactions.filter(x => x !== reaction);
- },
- }], ev.currentTarget ?? ev.target);
-}
-
-function preview(ev: MouseEvent) {
- os.popup(defineAsyncComponent(() => import('@/components/MkEmojiPickerDialog.vue')), {
- asReactionPicker: true,
- src: ev.currentTarget ?? ev.target,
- }, {}, 'closed');
-}
-
-async function setDefault() {
- const { canceled } = await os.confirm({
- type: 'warning',
- text: i18n.ts.resetAreYouSure,
- });
- if (canceled) return;
-
- reactions = deepClone(defaultStore.def.reactions.default);
-}
-
-function chooseEmoji(ev: MouseEvent) {
- os.pickEmoji(ev.currentTarget ?? ev.target, {
- showPinned: false,
- }).then(emoji => {
- if (!reactions.includes(emoji)) {
- reactions.push(emoji);
- }
- });
-}
-
-function chooseNewLike(ev: MouseEvent) {
- os.pickEmoji(ev.currentTarget ?? ev.target, {
- showPinned: false,
- }).then(async emoji => {
- defaultStore.set('like', emoji as string);
- await reloadAsk();
- });
-}
-
-async function resetLike() {
- defaultStore.set('like', null);
- await reloadAsk();
-}
-
-watch($$(reactions), () => {
- save();
-}, {
- deep: true,
-});
-
-const headerActions = $computed(() => []);
-
-const headerTabs = $computed(() => []);
-
-definePageMetadata({
- title: i18n.ts.reaction,
- icon: 'ph-smiley ph-bold ph-lg',
- action: {
- icon: 'ph-eye ph-bold ph-lg',
- handler: preview,
- },
-});
-</script>
-
-<style lang="scss" module>
-.reactions {
- padding: 12px;
- font-size: 1.1em;
-}
-
-.reactionsItem {
- display: inline-block;
- padding: 8px;
- cursor: move;
-}
-
-.reactionsAdd {
- display: inline-block;
- padding: 8px;
-}
-</style>
diff --git a/packages/frontend/src/pages/settings/roles.vue b/packages/frontend/src/pages/settings/roles.vue
index 291be4f3be..716b168c92 100644
--- a/packages/frontend/src/pages/settings/roles.vue
+++ b/packages/frontend/src/pages/settings/roles.vue
@@ -23,21 +23,12 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { computed, reactive, watch } from 'vue';
-import MkButton from '@/components/MkButton.vue';
-import MkInput from '@/components/MkInput.vue';
-import MkTextarea from '@/components/MkTextarea.vue';
-import MkSwitch from '@/components/MkSwitch.vue';
-import MkSelect from '@/components/MkSelect.vue';
-import FormSplit from '@/components/form/split.vue';
-import MkFolder from '@/components/MkFolder.vue';
-import FormSlot from '@/components/form/slot.vue';
+import { computed } from 'vue';
import FormSection from '@/components/form/section.vue';
import * as os from '@/os.js';
import { i18n } from '@/i18n.js';
import { $i } from '@/account.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';
-import { defaultStore } from '@/store.js';
import MkRolePreview from '@/components/MkRolePreview.vue';
function save() {
@@ -46,9 +37,9 @@ function save() {
});
}
-const headerActions = $computed(() => []);
+const headerActions = computed(() => []);
-const headerTabs = $computed(() => []);
+const headerTabs = computed(() => []);
definePageMetadata({
title: i18n.ts.roles,
diff --git a/packages/frontend/src/pages/settings/security.vue b/packages/frontend/src/pages/settings/security.vue
index 97bb84b655..9ae479e6e4 100644
--- a/packages/frontend/src/pages/settings/security.vue
+++ b/packages/frontend/src/pages/settings/security.vue
@@ -40,6 +40,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
+import { computed } from 'vue';
import X2fa from './2fa.vue';
import FormSection from '@/components/form/section.vue';
import FormSlot from '@/components/form/slot.vue';
@@ -97,9 +98,9 @@ async function regenerateToken() {
});
}
-const headerActions = $computed(() => []);
+const headerActions = computed(() => []);
-const headerTabs = $computed(() => []);
+const headerTabs = computed(() => []);
definePageMetadata({
title: i18n.ts.security,
diff --git a/packages/frontend/src/pages/settings/sounds.sound.vue b/packages/frontend/src/pages/settings/sounds.sound.vue
index 514ead4de1..a43ffb1f0b 100644
--- a/packages/frontend/src/pages/settings/sounds.sound.vue
+++ b/packages/frontend/src/pages/settings/sounds.sound.vue
@@ -7,8 +7,12 @@ SPDX-License-Identifier: AGPL-3.0-only
<div class="_gaps_m">
<MkSelect v-model="type">
<template #label>{{ i18n.ts.sound }}</template>
- <option v-for="x in soundsTypes" :key="x" :value="x">{{ x == null ? i18n.ts.none : x }}</option>
+ <option v-for="x in soundsTypes" :key="x ?? 'null'" :value="x">{{ getSoundTypeName(x) }}</option>
</MkSelect>
+ <div v-if="type === '_driveFile_'" :class="$style.fileSelectorRoot">
+ <MkButton :class="$style.fileSelectorButton" inline rounded primary @click="selectSound">{{ i18n.ts.selectFile }}</MkButton>
+ <div :class="['_nowrap', !fileUrl && $style.fileNotSelected]">{{ friendlyFileName }}</div>
+ </div>
<MkRange v-model="volume" :min="0" :max="1" :step="0.05" :textConverter="(v) => `${Math.floor(v * 100)}%`">
<template #label>{{ i18n.ts.volume }}</template>
</MkRange>
@@ -21,30 +25,149 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { } from 'vue';
+import { ref, computed } from 'vue';
+import type { SoundType } from '@/scripts/sound.js';
import MkSelect from '@/components/MkSelect.vue';
import MkButton from '@/components/MkButton.vue';
import MkRange from '@/components/MkRange.vue';
import { i18n } from '@/i18n.js';
-import { playFile, soundsTypes } from '@/scripts/sound.js';
+import * as os from '@/os.js';
+import { playFile, soundsTypes, getSoundDuration } from '@/scripts/sound.js';
+import { selectFile } from '@/scripts/select-file.js';
const props = defineProps<{
- type: string;
+ type: SoundType;
+ fileId?: string;
+ fileUrl?: string;
volume: number;
}>();
const emit = defineEmits<{
- (ev: 'update', result: { type: string; volume: number; }): void;
+ (ev: 'update', result: { type: SoundType; fileId?: string; fileUrl?: string; volume: number; }): void;
}>();
-let type = $ref(props.type);
-let volume = $ref(props.volume);
+const type = ref<SoundType>(props.type);
+const fileId = ref(props.fileId);
+const fileUrl = ref(props.fileUrl);
+const fileName = ref<string>('');
+const volume = ref(props.volume);
+
+if (type.value === '_driveFile_' && fileId.value) {
+ const apiRes = await os.api('drive/files/show', {
+ fileId: fileId.value,
+ });
+ fileName.value = apiRes.name;
+}
+
+function getSoundTypeName(f: SoundType): string {
+ switch (f) {
+ case null:
+ return i18n.ts.none;
+ case '_driveFile_':
+ return i18n.ts._soundSettings.driveFile;
+ default:
+ return f;
+ }
+}
+
+const friendlyFileName = computed<string>(() => {
+ if (fileName.value) {
+ return fileName.value;
+ }
+ if (fileUrl.value) {
+ return fileUrl.value;
+ }
+
+ return i18n.ts._soundSettings.driveFileWarn;
+});
+
+function selectSound(ev) {
+ selectFile(ev.currentTarget ?? ev.target, i18n.ts._soundSettings.driveFile).then(async (file) => {
+ if (!file.type.startsWith('audio')) {
+ os.alert({
+ type: 'warning',
+ title: i18n.ts._soundSettings.driveFileTypeWarn,
+ text: i18n.ts._soundSettings.driveFileTypeWarnDescription,
+ });
+ return;
+ }
+ const duration = await getSoundDuration(file.url);
+ if (duration >= 2000) {
+ const { canceled } = await os.confirm({
+ type: 'warning',
+ title: i18n.ts._soundSettings.driveFileDurationWarn,
+ text: i18n.ts._soundSettings.driveFileDurationWarnDescription,
+ okText: i18n.ts.continue,
+ cancelText: i18n.ts.cancel,
+ });
+ if (canceled) return;
+ }
+
+ fileUrl.value = file.url;
+ fileName.value = file.name;
+ fileId.value = file.id;
+ });
+}
function listen() {
- playFile(type, volume);
+ if (type.value === '_driveFile_' && (!fileUrl.value || !fileId.value)) {
+ os.alert({
+ type: 'warning',
+ text: i18n.ts._soundSettings.driveFileWarn,
+ });
+ return;
+ }
+
+ playFile(type.value === '_driveFile_' ? {
+ type: '_driveFile_',
+ fileId: fileId.value as string,
+ fileUrl: fileUrl.value as string,
+ volume: volume.value,
+ } : {
+ type: type.value,
+ volume: volume.value,
+ });
}
function save() {
- emit('update', { type, volume });
+ if (type.value === '_driveFile_' && !fileUrl.value) {
+ os.alert({
+ type: 'warning',
+ text: i18n.ts._soundSettings.driveFileWarn,
+ });
+ return;
+ }
+
+ if (type.value !== '_driveFile_') {
+ fileUrl.value = undefined;
+ fileName.value = '';
+ fileId.value = undefined;
+ }
+
+ emit('update', {
+ type: type.value,
+ fileId: fileId.value,
+ fileUrl: fileUrl.value,
+ volume: volume.value,
+ });
+
+ os.success();
}
</script>
+
+<style module>
+.fileSelectorRoot {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+}
+
+.fileSelectorButton {
+ flex-shrink: 0;
+}
+
+.fileNotSelected {
+ font-weight: 700;
+ color: var(--infoWarnFg);
+}
+</style>
diff --git a/packages/frontend/src/pages/settings/sounds.vue b/packages/frontend/src/pages/settings/sounds.vue
index 89241b8c02..bec41a6cec 100644
--- a/packages/frontend/src/pages/settings/sounds.vue
+++ b/packages/frontend/src/pages/settings/sounds.vue
@@ -5,6 +5,12 @@ SPDX-License-Identifier: AGPL-3.0-only
<template>
<div class="_gaps_m">
+ <MkSwitch v-model="notUseSound">
+ <template #label>{{ i18n.ts.notUseSound }}</template>
+ </MkSwitch>
+ <MkSwitch v-model="useSoundOnlyWhenActive">
+ <template #label>{{ i18n.ts.useSoundOnlyWhenActive }}</template>
+ </MkSwitch>
<MkRange v-model="masterVolume" :min="0" :max="1" :step="0.05" :textConverter="(v) => `${Math.floor(v * 100)}%`">
<template #label>{{ i18n.ts.masterVolume }}</template>
</MkRange>
@@ -12,11 +18,11 @@ SPDX-License-Identifier: AGPL-3.0-only
<FormSection>
<template #label>{{ i18n.ts.sounds }}</template>
<div class="_gaps_s">
- <MkFolder v-for="type in soundsKeys" :key="type">
+ <MkFolder v-for="type in operationTypes" :key="type">
<template #label>{{ i18n.t('_sfx.' + type) }}</template>
- <template #suffix>{{ sounds[type].type ?? i18n.ts.none }}</template>
+ <template #suffix>{{ getSoundTypeName(sounds[type].type) }}</template>
- <XSound :type="sounds[type].type" :volume="sounds[type].volume" @update="(res) => updated(type, res)"/>
+ <XSound :type="sounds[type].type" :volume="sounds[type].volume" :fileId="sounds[type].fileId" :fileUrl="sounds[type].fileUrl" @update="(res) => updated(type, res)"/>
</MkFolder>
</div>
</FormSection>
@@ -27,6 +33,8 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { Ref, computed, ref } from 'vue';
+import type { SoundType, OperationType } from '@/scripts/sound.js';
+import type { SoundStore } from '@/store.js';
import XSound from './sounds.sound.vue';
import MkRange from '@/components/MkRange.vue';
import MkButton from '@/components/MkButton.vue';
@@ -34,23 +42,39 @@ import FormSection from '@/components/form/section.vue';
import MkFolder from '@/components/MkFolder.vue';
import { i18n } from '@/i18n.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';
+import { operationTypes } from '@/scripts/sound.js';
import { defaultStore } from '@/store.js';
+import MkSwitch from '@/components/MkSwitch.vue';
+const notUseSound = computed(defaultStore.makeGetterSetter('sound_notUseSound'));
+const useSoundOnlyWhenActive = computed(defaultStore.makeGetterSetter('sound_useSoundOnlyWhenActive'));
const masterVolume = computed(defaultStore.makeGetterSetter('sound_masterVolume'));
-const soundsKeys = ['note', 'noteMy', 'notification', 'antenna', 'channel'] as const;
-
-const sounds = ref<Record<typeof soundsKeys[number], Ref<any>>>({
+const sounds = ref<Record<OperationType, Ref<SoundStore>>>({
note: defaultStore.reactiveState.sound_note,
noteMy: defaultStore.reactiveState.sound_noteMy,
notification: defaultStore.reactiveState.sound_notification,
antenna: defaultStore.reactiveState.sound_antenna,
channel: defaultStore.reactiveState.sound_channel,
+ reaction: defaultStore.reactiveState.sound_reaction,
});
+function getSoundTypeName(f: SoundType): string {
+ switch (f) {
+ case null:
+ return i18n.ts.none;
+ case '_driveFile_':
+ return i18n.ts._soundSettings.driveFile;
+ default:
+ return f;
+ }
+}
+
async function updated(type: keyof typeof sounds.value, sound) {
- const v = {
+ const v: SoundStore = {
type: sound.type,
+ fileId: sound.fileId,
+ fileUrl: sound.fileUrl,
volume: sound.volume,
};
@@ -66,9 +90,9 @@ function reset() {
}
}
-const headerActions = $computed(() => []);
+const headerActions = computed(() => []);
-const headerTabs = $computed(() => []);
+const headerTabs = computed(() => []);
definePageMetadata({
title: i18n.ts.sounds,
diff --git a/packages/frontend/src/pages/settings/statusbar.vue b/packages/frontend/src/pages/settings/statusbar.vue
index 1999c7e20f..ae6a2deaf9 100644
--- a/packages/frontend/src/pages/settings/statusbar.vue
+++ b/packages/frontend/src/pages/settings/statusbar.vue
@@ -15,7 +15,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { onMounted } from 'vue';
+import { onMounted, ref, computed } from 'vue';
import { v4 as uuid } from 'uuid';
import XStatusbar from './statusbar.statusbar.vue';
import MkFolder from '@/components/MkFolder.vue';
@@ -27,11 +27,11 @@ import { definePageMetadata } from '@/scripts/page-metadata.js';
const statusbars = defaultStore.reactiveState.statusbars;
-let userLists = $ref();
+const userLists = ref();
onMounted(() => {
os.api('users/lists/list').then(res => {
- userLists = res;
+ userLists.value = res;
});
});
@@ -45,9 +45,9 @@ async function add() {
});
}
-const headerActions = $computed(() => []);
+const headerActions = computed(() => []);
-const headerTabs = $computed(() => []);
+const headerTabs = computed(() => []);
definePageMetadata({
title: i18n.ts.statusbar,
diff --git a/packages/frontend/src/pages/settings/theme.install.vue b/packages/frontend/src/pages/settings/theme.install.vue
index 9931bf8a76..d3ba9bc0aa 100644
--- a/packages/frontend/src/pages/settings/theme.install.vue
+++ b/packages/frontend/src/pages/settings/theme.install.vue
@@ -5,9 +5,9 @@ SPDX-License-Identifier: AGPL-3.0-only
<template>
<div class="_gaps_m">
- <MkTextarea v-model="installThemeCode">
+ <MkCodeEditor v-model="installThemeCode" lang="json5">
<template #label>{{ i18n.ts._theme.code }}</template>
- </MkTextarea>
+ </MkCodeEditor>
<div class="_buttons">
<MkButton :disabled="installThemeCode == null" inline @click="() => previewTheme(installThemeCode)"><i class="ph-eye ph-bold ph-lg"></i> {{ i18n.ts.preview }}</MkButton>
@@ -17,15 +17,15 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { } from 'vue';
-import MkTextarea from '@/components/MkTextarea.vue';
+import { ref, computed } from 'vue';
+import MkCodeEditor from '@/components/MkCodeEditor.vue';
import MkButton from '@/components/MkButton.vue';
import { parseThemeCode, previewTheme, installTheme } from '@/scripts/install-theme.js';
import * as os from '@/os.js';
import { i18n } from '@/i18n.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';
-let installThemeCode = $ref(null);
+const installThemeCode = ref(null);
async function install(code: string): Promise<void> {
try {
@@ -55,9 +55,9 @@ async function install(code: string): Promise<void> {
}
}
-const headerActions = $computed(() => []);
+const headerActions = computed(() => []);
-const headerTabs = $computed(() => []);
+const headerTabs = computed(() => []);
definePageMetadata({
title: i18n.ts._theme.install,
diff --git a/packages/frontend/src/pages/settings/theme.manage.vue b/packages/frontend/src/pages/settings/theme.manage.vue
index 484e83be4b..a2ecd0574f 100644
--- a/packages/frontend/src/pages/settings/theme.manage.vue
+++ b/packages/frontend/src/pages/settings/theme.manage.vue
@@ -72,9 +72,9 @@ function uninstall() {
os.success();
}
-const headerActions = $computed(() => []);
+const headerActions = computed(() => []);
-const headerTabs = $computed(() => []);
+const headerTabs = computed(() => []);
definePageMetadata({
title: i18n.ts._theme.manage,
diff --git a/packages/frontend/src/pages/settings/theme.vue b/packages/frontend/src/pages/settings/theme.vue
index eb69912609..58c0b8fc82 100644
--- a/packages/frontend/src/pages/settings/theme.vue
+++ b/packages/frontend/src/pages/settings/theme.vue
@@ -160,9 +160,9 @@ function setWallpaper(event) {
});
}
-const headerActions = $computed(() => []);
+const headerActions = computed(() => []);
-const headerTabs = $computed(() => []);
+const headerTabs = computed(() => []);
definePageMetadata({
title: i18n.ts.theme,
diff --git a/packages/frontend/src/pages/settings/webhook.edit.vue b/packages/frontend/src/pages/settings/webhook.edit.vue
index d65dcae538..f6e2f63317 100644
--- a/packages/frontend/src/pages/settings/webhook.edit.vue
+++ b/packages/frontend/src/pages/settings/webhook.edit.vue
@@ -42,7 +42,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { } from 'vue';
+import { ref, computed } from 'vue';
import MkInput from '@/components/MkInput.vue';
import FormSection from '@/components/form/section.vue';
import MkSwitch from '@/components/MkSwitch.vue';
@@ -62,36 +62,36 @@ const webhook = await os.api('i/webhooks/show', {
webhookId: props.webhookId,
});
-let name = $ref(webhook.name);
-let url = $ref(webhook.url);
-let secret = $ref(webhook.secret);
-let active = $ref(webhook.active);
+const name = ref(webhook.name);
+const url = ref(webhook.url);
+const secret = ref(webhook.secret);
+const active = ref(webhook.active);
-let event_follow = $ref(webhook.on.includes('follow'));
-let event_followed = $ref(webhook.on.includes('followed'));
-let event_note = $ref(webhook.on.includes('note'));
-let event_reply = $ref(webhook.on.includes('reply'));
-let event_renote = $ref(webhook.on.includes('renote'));
-let event_reaction = $ref(webhook.on.includes('reaction'));
-let event_mention = $ref(webhook.on.includes('mention'));
+const event_follow = ref(webhook.on.includes('follow'));
+const event_followed = ref(webhook.on.includes('followed'));
+const event_note = ref(webhook.on.includes('note'));
+const event_reply = ref(webhook.on.includes('reply'));
+const event_renote = ref(webhook.on.includes('renote'));
+const event_reaction = ref(webhook.on.includes('reaction'));
+const event_mention = ref(webhook.on.includes('mention'));
async function save(): Promise<void> {
const events = [];
- if (event_follow) events.push('follow');
- if (event_followed) events.push('followed');
- if (event_note) events.push('note');
- if (event_reply) events.push('reply');
- if (event_renote) events.push('renote');
- if (event_reaction) events.push('reaction');
- if (event_mention) events.push('mention');
+ if (event_follow.value) events.push('follow');
+ if (event_followed.value) events.push('followed');
+ if (event_note.value) events.push('note');
+ if (event_reply.value) events.push('reply');
+ if (event_renote.value) events.push('renote');
+ if (event_reaction.value) events.push('reaction');
+ if (event_mention.value) events.push('mention');
os.apiWithDialog('i/webhooks/update', {
- name,
- url,
- secret,
+ name: name.value,
+ url: url.value,
+ secret: secret.value,
webhookId: props.webhookId,
on: events,
- active,
+ active: active.value,
});
}
@@ -109,9 +109,9 @@ async function del(): Promise<void> {
router.push('/settings/webhook');
}
-const headerActions = $computed(() => []);
+const headerActions = computed(() => []);
-const headerTabs = $computed(() => []);
+const headerTabs = computed(() => []);
definePageMetadata({
title: 'Edit webhook',
diff --git a/packages/frontend/src/pages/settings/webhook.new.vue b/packages/frontend/src/pages/settings/webhook.new.vue
index f08f143d46..032796caf0 100644
--- a/packages/frontend/src/pages/settings/webhook.new.vue
+++ b/packages/frontend/src/pages/settings/webhook.new.vue
@@ -39,7 +39,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { } from 'vue';
+import { ref, computed } from 'vue';
import MkInput from '@/components/MkInput.vue';
import FormSection from '@/components/form/section.vue';
import MkSwitch from '@/components/MkSwitch.vue';
@@ -48,39 +48,39 @@ import * as os from '@/os.js';
import { i18n } from '@/i18n.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';
-let name = $ref('');
-let url = $ref('');
-let secret = $ref('');
+const name = ref('');
+const url = ref('');
+const secret = ref('');
-let event_follow = $ref(true);
-let event_followed = $ref(true);
-let event_note = $ref(true);
-let event_reply = $ref(true);
-let event_renote = $ref(true);
-let event_reaction = $ref(true);
-let event_mention = $ref(true);
+const event_follow = ref(true);
+const event_followed = ref(true);
+const event_note = ref(true);
+const event_reply = ref(true);
+const event_renote = ref(true);
+const event_reaction = ref(true);
+const event_mention = ref(true);
async function create(): Promise<void> {
const events = [];
- if (event_follow) events.push('follow');
- if (event_followed) events.push('followed');
- if (event_note) events.push('note');
- if (event_reply) events.push('reply');
- if (event_renote) events.push('renote');
- if (event_reaction) events.push('reaction');
- if (event_mention) events.push('mention');
+ if (event_follow.value) events.push('follow');
+ if (event_followed.value) events.push('followed');
+ if (event_note.value) events.push('note');
+ if (event_reply.value) events.push('reply');
+ if (event_renote.value) events.push('renote');
+ if (event_reaction.value) events.push('reaction');
+ if (event_mention.value) events.push('mention');
os.apiWithDialog('i/webhooks/create', {
- name,
- url,
- secret,
+ name: name.value,
+ url: url.value,
+ secret: secret.value,
on: events,
});
}
-const headerActions = $computed(() => []);
+const headerActions = computed(() => []);
-const headerTabs = $computed(() => []);
+const headerTabs = computed(() => []);
definePageMetadata({
title: 'Create new webhook',
diff --git a/packages/frontend/src/pages/settings/webhook.vue b/packages/frontend/src/pages/settings/webhook.vue
index e9a7f9a02e..c391458274 100644
--- a/packages/frontend/src/pages/settings/webhook.vue
+++ b/packages/frontend/src/pages/settings/webhook.vue
@@ -33,7 +33,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { } from 'vue';
+import { computed } from 'vue';
import MkPagination from '@/components/MkPagination.vue';
import FormSection from '@/components/form/section.vue';
import FormLink from '@/components/form/link.vue';
@@ -46,9 +46,9 @@ const pagination = {
noPaging: true,
};
-const headerActions = $computed(() => []);
+const headerActions = computed(() => []);
-const headerTabs = $computed(() => []);
+const headerTabs = computed(() => []);
definePageMetadata({
title: 'Webhook',
diff --git a/packages/frontend/src/pages/share.vue b/packages/frontend/src/pages/share.vue
index 6d0492250d..a978be0ae5 100644
--- a/packages/frontend/src/pages/share.vue
+++ b/packages/frontend/src/pages/share.vue
@@ -19,7 +19,7 @@ SPDX-License-Identifier: AGPL-3.0-only
:renote="renote"
:initialVisibleUsers="visibleUsers"
class="_panel"
- @posted="state = 'posted'"
+ @posted="onPosted"
/>
<div v-else-if="state === 'posted'" class="_buttonsCenter">
<MkButton primary @click="close">{{ i18n.ts.close }}</MkButton>
@@ -30,43 +30,43 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-// SPECIFICATION: https://misskey-hub.net/docs/features/share-form.html
+// SPECIFICATION: https://misskey-hub.net/docs/for-users/features/share-form/
-import { } from 'vue';
+import { ref, computed } from 'vue';
import * as Misskey from 'misskey-js';
import MkButton from '@/components/MkButton.vue';
import MkPostForm from '@/components/MkPostForm.vue';
import * as os from '@/os.js';
-import { mainRouter } from '@/router.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';
+import { postMessageToParentWindow } from '@/scripts/post-message.js';
import { i18n } from '@/i18n.js';
const urlParams = new URLSearchParams(window.location.search);
const localOnlyQuery = urlParams.get('localOnly');
const visibilityQuery = urlParams.get('visibility') as typeof Misskey.noteVisibilities[number];
-let state = $ref('fetching' as 'fetching' | 'writing' | 'posted');
-let title = $ref(urlParams.get('title'));
+const state = ref<'fetching' | 'writing' | 'posted'>('fetching');
+const title = ref(urlParams.get('title'));
const text = urlParams.get('text');
const url = urlParams.get('url');
-let initialText = $ref<string | undefined>();
-let reply = $ref<Misskey.entities.Note | undefined>();
-let renote = $ref<Misskey.entities.Note | undefined>();
-let visibility = $ref(Misskey.noteVisibilities.includes(visibilityQuery) ? visibilityQuery : undefined);
-let localOnly = $ref(localOnlyQuery === '0' ? false : localOnlyQuery === '1' ? true : undefined);
-let files = $ref([] as Misskey.entities.DriveFile[]);
-let visibleUsers = $ref([] as Misskey.entities.User[]);
+const initialText = ref<string | undefined>();
+const reply = ref<Misskey.entities.Note | undefined>();
+const renote = ref<Misskey.entities.Note | undefined>();
+const visibility = ref(Misskey.noteVisibilities.includes(visibilityQuery) ? visibilityQuery : undefined);
+const localOnly = ref(localOnlyQuery === '0' ? false : localOnlyQuery === '1' ? true : undefined);
+const files = ref([] as Misskey.entities.DriveFile[]);
+const visibleUsers = ref([] as Misskey.entities.User[]);
async function init() {
let noteText = '';
- if (title) noteText += `[ ${title} ]\n`;
+ if (title.value) noteText += `[ ${title.value} ]\n`;
// Googleニュース対策
- if (text?.startsWith(`${title}.\n`)) noteText += text.replace(`${title}.\n`, '');
- else if (text && title !== text) noteText += `${text}\n`;
+ if (text?.startsWith(`${title.value}.\n`)) noteText += text.replace(`${title.value}.\n`, '');
+ else if (text && title.value !== text) noteText += `${text}\n`;
if (url) noteText += `${url}`;
- initialText = noteText.trim();
+ initialText.value = noteText.trim();
- if (visibility === 'specified') {
+ if (visibility.value === 'specified') {
const visibleUserIds = urlParams.get('visibleUserIds');
const visibleAccts = urlParams.get('visibleAccts');
await Promise.all(
@@ -78,7 +78,7 @@ async function init() {
.map(q => 'username' in q ? { username: q.username, host: q.host === null ? undefined : q.host } : q)
.map(q => os.api('users/show', q)
.then(user => {
- visibleUsers.push(user);
+ visibleUsers.value.push(user);
}, () => {
console.error(`Invalid user query: ${JSON.stringify(q)}`);
}),
@@ -91,7 +91,7 @@ async function init() {
const replyId = urlParams.get('replyId');
const replyUri = urlParams.get('replyUri');
if (replyId) {
- reply = await os.api('notes/show', {
+ reply.value = await os.api('notes/show', {
noteId: replyId,
});
} else if (replyUri) {
@@ -99,7 +99,7 @@ async function init() {
uri: replyUri,
});
if (obj.type === 'Note') {
- reply = obj.object;
+ reply.value = obj.object;
}
}
//#endregion
@@ -108,7 +108,7 @@ async function init() {
const renoteId = urlParams.get('renoteId');
const renoteUri = urlParams.get('renoteUri');
if (renoteId) {
- renote = await os.api('notes/show', {
+ renote.value = await os.api('notes/show', {
noteId: renoteId,
});
} else if (renoteUri) {
@@ -116,7 +116,7 @@ async function init() {
uri: renoteUri,
});
if (obj.type === 'Note') {
- renote = obj.object;
+ renote.value = obj.object;
}
}
//#endregion
@@ -128,7 +128,7 @@ async function init() {
fileIds.split(',')
.map(fileId => os.api('drive/files/show', { fileId })
.then(file => {
- files.push(file);
+ files.value.push(file);
}, () => {
console.error(`Failed to fetch a file ${fileId}`);
}),
@@ -144,7 +144,7 @@ async function init() {
});
}
- state = 'writing';
+ state.value = 'writing';
}
init();
@@ -162,9 +162,14 @@ function goToMisskey(): void {
location.href = '/';
}
-const headerActions = $computed(() => []);
+function onPosted(): void {
+ state.value = 'posted';
+ postMessageToParentWindow('misskey:shareForm:shareCompleted');
+}
+
+const headerActions = computed(() => []);
-const headerTabs = $computed(() => []);
+const headerTabs = computed(() => []);
definePageMetadata({
title: i18n.ts.share,
diff --git a/packages/frontend/src/pages/signup-complete.vue b/packages/frontend/src/pages/signup-complete.vue
index 0bb6548447..4009652bcf 100644
--- a/packages/frontend/src/pages/signup-complete.vue
+++ b/packages/frontend/src/pages/signup-complete.vue
@@ -25,22 +25,22 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { } from 'vue';
+import { ref } from 'vue';
import MkButton from '@/components/MkButton.vue';
import MkAnimBg from '@/components/MkAnimBg.vue';
import { login } from '@/account.js';
import { i18n } from '@/i18n.js';
import * as os from '@/os.js';
-let submitting = $ref(false);
+const submitting = ref(false);
const props = defineProps<{
code: string;
}>();
function submit() {
- if (submitting) return;
- submitting = true;
+ if (submitting.value) return;
+ submitting.value = true;
os.api('signup-pending', {
code: props.code,
@@ -54,7 +54,7 @@ function submit() {
}
return login(res.i, '/');
}).catch(() => {
- submitting = false;
+ submitting.value = false;
os.alert({
type: 'error',
diff --git a/packages/frontend/src/pages/tag.vue b/packages/frontend/src/pages/tag.vue
index 7ac485685e..167816638c 100644
--- a/packages/frontend/src/pages/tag.vue
+++ b/packages/frontend/src/pages/tag.vue
@@ -51,9 +51,9 @@ async function post() {
notes.value?.pagingComponent?.reload();
}
-const headerActions = $computed(() => []);
+const headerActions = computed(() => []);
-const headerTabs = $computed(() => []);
+const headerTabs = computed(() => []);
definePageMetadata(computed(() => ({
title: props.tag,
diff --git a/packages/frontend/src/pages/theme-editor.vue b/packages/frontend/src/pages/theme-editor.vue
index 1bc5eaddd9..6c4a54eb5c 100644
--- a/packages/frontend/src/pages/theme-editor.vue
+++ b/packages/frontend/src/pages/theme-editor.vue
@@ -51,9 +51,9 @@ SPDX-License-Identifier: AGPL-3.0-only
<template #label>{{ i18n.ts.editCode }}</template>
<div class="_gaps_m">
- <MkTextarea v-model="themeCode" tall>
+ <MkCodeEditor v-model="themeCode" lang="json5">
<template #label>{{ i18n.ts._theme.code }}</template>
- </MkTextarea>
+ </MkCodeEditor>
<MkButton primary @click="applyThemeCode">{{ i18n.ts.apply }}</MkButton>
</div>
</MkFolder>
@@ -73,13 +73,14 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { watch } from 'vue';
+import { watch, ref, computed } from 'vue';
import { toUnicode } from 'punycode/';
import tinycolor from 'tinycolor2';
import { v4 as uuid } from 'uuid';
import JSON5 from 'json5';
import MkButton from '@/components/MkButton.vue';
+import MkCodeEditor from '@/components/MkCodeEditor.vue';
import MkTextarea from '@/components/MkTextarea.vue';
import MkFolder from '@/components/MkFolder.vue';
@@ -124,53 +125,53 @@ const fgColors = [
{ color: 'pink', forLight: '#84667d', forDark: '#e4d1e0', forPreview: '#b12390' },
];
-let theme = $ref<Partial<Theme>>({
+const theme = ref<Partial<Theme>>({
base: 'light',
props: lightTheme.props,
});
-let description = $ref<string | null>(null);
-let themeCode = $ref<string | null>(null);
-let changed = $ref(false);
+const description = ref<string | null>(null);
+const themeCode = ref<string | null>(null);
+const changed = ref(false);
-useLeaveGuard($$(changed));
+useLeaveGuard(changed);
function setBgColor(color: typeof bgColors[number]) {
- if (theme.base !== color.kind) {
+ if (theme.value.base !== color.kind) {
const base = color.kind === 'dark' ? darkTheme : lightTheme;
for (const prop of Object.keys(base.props)) {
if (prop === 'accent') continue;
if (prop === 'fg') continue;
- theme.props[prop] = base.props[prop];
+ theme.value.props[prop] = base.props[prop];
}
}
- theme.base = color.kind;
- theme.props.bg = color.color;
+ theme.value.base = color.kind;
+ theme.value.props.bg = color.color;
- if (theme.props.fg) {
- const matchedFgColor = fgColors.find(x => [tinycolor(x.forLight).toRgbString(), tinycolor(x.forDark).toRgbString()].includes(tinycolor(theme.props.fg).toRgbString()));
+ if (theme.value.props.fg) {
+ const matchedFgColor = fgColors.find(x => [tinycolor(x.forLight).toRgbString(), tinycolor(x.forDark).toRgbString()].includes(tinycolor(theme.value.props.fg).toRgbString()));
if (matchedFgColor) setFgColor(matchedFgColor);
}
}
function setAccentColor(color) {
- theme.props.accent = color;
+ theme.value.props.accent = color;
}
function setFgColor(color) {
- theme.props.fg = theme.base === 'light' ? color.forLight : color.forDark;
+ theme.value.props.fg = theme.value.base === 'light' ? color.forLight : color.forDark;
}
function apply() {
- themeCode = JSON5.stringify(theme, null, '\t');
- applyTheme(theme, false);
- changed = true;
+ themeCode.value = JSON5.stringify(theme.value, null, '\t');
+ applyTheme(theme.value, false);
+ changed.value = true;
}
function applyThemeCode() {
let parsed;
try {
- parsed = JSON5.parse(themeCode);
+ parsed = JSON5.parse(themeCode.value);
} catch (err) {
os.alert({
type: 'error',
@@ -179,7 +180,7 @@ function applyThemeCode() {
return;
}
- theme = parsed;
+ theme.value = parsed;
}
async function saveAs() {
@@ -189,34 +190,34 @@ async function saveAs() {
});
if (canceled) return;
- theme.id = uuid();
- theme.name = name;
- theme.author = `@${$i.username}@${toUnicode(host)}`;
- if (description) theme.desc = description;
- await addTheme(theme);
- applyTheme(theme);
+ theme.value.id = uuid();
+ theme.value.name = name;
+ theme.value.author = `@${$i.username}@${toUnicode(host)}`;
+ if (description.value) theme.value.desc = description.value;
+ await addTheme(theme.value);
+ applyTheme(theme.value);
if (defaultStore.state.darkMode) {
- ColdDeviceStorage.set('darkTheme', theme);
+ ColdDeviceStorage.set('darkTheme', theme.value);
} else {
- ColdDeviceStorage.set('lightTheme', theme);
+ ColdDeviceStorage.set('lightTheme', theme.value);
}
- changed = false;
+ changed.value = false;
os.alert({
type: 'success',
- text: i18n.t('_theme.installed', { name: theme.name }),
+ text: i18n.t('_theme.installed', { name: theme.value.name }),
});
}
-watch($$(theme), apply, { deep: true });
+watch(theme, apply, { deep: true });
-const headerActions = $computed(() => [{
+const headerActions = computed(() => [{
asFullButton: true,
icon: 'ph-check ph-bold ph-lg',
text: i18n.ts.saveAs,
handler: saveAs,
}]);
-const headerTabs = $computed(() => []);
+const headerTabs = computed(() => []);
definePageMetadata({
title: i18n.ts.themeEditor,
diff --git a/packages/frontend/src/pages/timeline.vue b/packages/frontend/src/pages/timeline.vue
index 2f63ec9a38..f9adee94dc 100644
--- a/packages/frontend/src/pages/timeline.vue
+++ b/packages/frontend/src/pages/timeline.vue
@@ -34,7 +34,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { computed, watch, provide } from 'vue';
+import { computed, watch, provide, shallowRef, ref } from 'vue';
import type { Tab } from '@/components/global/MkPageHeader.tabs.vue';
import MkTimeline from '@/components/MkTimeline.vue';
import MkInfo from '@/components/MkInfo.vue';
@@ -46,9 +46,10 @@ import { i18n } from '@/i18n.js';
import { instance } from '@/instance.js';
import { $i } from '@/account.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';
-import { miLocalStorage } from '@/local-storage.js';
import { antennasCache, userListsCache } from '@/cache.js';
import { deviceKind } from '@/scripts/device-kind.js';
+import { MenuItem } from '@/types/menu.js';
+import { miLocalStorage } from '@/local-storage.js';
provide('shouldOmitHeaderTitle', true);
@@ -59,49 +60,67 @@ const keymap = {
't': focus,
};
-const tlComponent = $shallowRef<InstanceType<typeof MkTimeline>>();
-const rootEl = $shallowRef<HTMLElement>();
+const tlComponent = shallowRef<InstanceType<typeof MkTimeline>>();
+const rootEl = shallowRef<HTMLElement>();
-let queue = $ref(0);
-let srcWhenNotSignin = $ref(isLocalTimelineAvailable ? 'local' : 'global');
-const src = $computed({ get: () => ($i ? defaultStore.reactiveState.tl.value.src : srcWhenNotSignin), set: (x) => saveSrc(x) });
-const withRenotes = $ref(true);
-const withReplies = $ref($i ? defaultStore.state.tlWithReplies : false);
-const withBots = $ref($i ? defaultStore.state.tlWithBots : true);
-const onlyFiles = $ref(false);
+const queue = ref(0);
+const srcWhenNotSignin = ref(isLocalTimelineAvailable ? 'local' : 'global');
+const src = computed({ get: () => ($i ? defaultStore.reactiveState.tl.value.src : srcWhenNotSignin.value), set: (x) => saveSrc(x) });
+const withRenotes = ref(true);
+const withReplies = ref($i ? defaultStore.state.tlWithReplies : false);
+const withBots = ref($i ? defaultStore.state.tlWithBots : true);
+const onlyFiles = ref(false);
-watch($$(src), () => queue = 0);
+watch(src, () => queue.value = 0);
-watch($$(withReplies), (x) => {
+watch(withReplies, (x) => {
if ($i) defaultStore.set('tlWithReplies', x);
});
function queueUpdated(q: number): void {
- queue = q;
+ queue.value = q;
}
function top(): void {
- if (rootEl) scroll(rootEl, { top: 0 });
+ if (rootEl.value) scroll(rootEl.value, { top: 0 });
}
async function chooseList(ev: MouseEvent): Promise<void> {
const lists = await userListsCache.fetch();
- const items = lists.map(list => ({
- type: 'link' as const,
- text: list.name,
- to: `/timeline/list/${list.id}`,
- }));
+ const items: MenuItem[] = [
+ ...lists.map(list => ({
+ type: 'link' as const,
+ text: list.name,
+ to: `/timeline/list/${list.id}`,
+ })),
+ (lists.length === 0 ? undefined : { type: 'divider' }),
+ {
+ type: 'link' as const,
+ icon: 'ti ti-plus',
+ text: i18n.ts.createNew,
+ to: '/my/lists',
+ },
+ ];
os.popupMenu(items, ev.currentTarget ?? ev.target);
}
async function chooseAntenna(ev: MouseEvent): Promise<void> {
const antennas = await antennasCache.fetch();
- const items = antennas.map(antenna => ({
- type: 'link' as const,
- text: antenna.name,
- indicate: antenna.hasUnreadNote,
- to: `/timeline/antenna/${antenna.id}`,
- }));
+ const items: MenuItem[] = [
+ ...antennas.map(antenna => ({
+ type: 'link' as const,
+ text: antenna.name,
+ indicate: antenna.hasUnreadNote,
+ to: `/timeline/antenna/${antenna.id}`,
+ })),
+ (antennas.length === 0 ? undefined : { type: 'divider' }),
+ {
+ type: 'link' as const,
+ icon: 'ti ti-plus',
+ text: i18n.ts.createNew,
+ to: '/my/antennas',
+ },
+ ];
os.popupMenu(items, ev.currentTarget ?? ev.target);
}
@@ -109,16 +128,30 @@ async function chooseChannel(ev: MouseEvent): Promise<void> {
const channels = await os.api('channels/my-favorites', {
limit: 100,
});
- const items = channels.map(channel => ({
- type: 'link' as const,
- text: channel.name,
- indicate: channel.hasUnreadNote,
- to: `/channels/${channel.id}`,
- }));
+ const items: MenuItem[] = [
+ ...channels.map(channel => {
+ const lastReadedAt = miLocalStorage.getItemAsJson(`channelLastReadedAt:${channel.id}`) ?? null;
+ const hasUnreadNote = (lastReadedAt && channel.lastNotedAt) ? Date.parse(channel.lastNotedAt) > lastReadedAt : !!(!lastReadedAt && channel.lastNotedAt);
+
+ return {
+ type: 'link' as const,
+ text: channel.name,
+ indicate: hasUnreadNote,
+ to: `/channels/${channel.id}`,
+ };
+ }),
+ (channels.length === 0 ? undefined : { type: 'divider' }),
+ {
+ type: 'link' as const,
+ icon: 'ti ti-plus',
+ text: i18n.ts.createNew,
+ to: '/channels',
+ },
+ ];
os.popupMenu(items, ev.currentTarget ?? ev.target);
}
-function saveSrc(newSrc: 'home' | 'local' | 'social' | 'global' | `list:${string}`): void {
+function saveSrc(newSrc: 'home' | 'local' | 'social' | 'global' | 'bubble' | `list:${string}`): void {
let userList = null;
if (newSrc.startsWith('userList:')) {
const id = newSrc.substring('userList:'.length);
@@ -128,7 +161,7 @@ function saveSrc(newSrc: 'home' | 'local' | 'social' | 'global' | `list:${string
src: newSrc,
userList,
});
- srcWhenNotSignin = newSrc;
+ srcWhenNotSignin.value = newSrc;
}
async function timetravel(): Promise<void> {
@@ -137,21 +170,21 @@ async function timetravel(): Promise<void> {
});
if (canceled) return;
- tlComponent.timetravel(date);
+ tlComponent.value.timetravel(date);
}
function focus(): void {
- tlComponent.focus();
+ tlComponent.value.focus();
}
function closeTutorial(): void {
- if (!['home', 'local', 'social', 'global'].includes(src)) return;
+ if (!['home', 'local', 'social', 'global'].includes(src.value)) return;
const before = defaultStore.state.timelineTutorials;
- before[src] = true;
+ before[src.value] = true;
defaultStore.set('timelineTutorials', before);
}
-const headerActions = $computed(() => {
+const headerActions = computed(() => {
const tmp = [
{
icon: 'ph-dots-three ph-bold ph-lg',
@@ -160,17 +193,17 @@ const headerActions = $computed(() => {
os.popupMenu([{
type: 'switch',
text: i18n.ts.showRenotes,
- icon: 'ph-rocket-launch ph-bold ph-lg',
- ref: $$(withRenotes),
- }, src === 'local' || src === 'social' ? {
+ ref: withRenotes,
+ }, src.value === 'local' || src.value === 'social' ? {
type: 'switch',
text: i18n.ts.showRepliesToOthersInTimeline,
- ref: $$(withReplies),
+ ref: withReplies,
+ disabled: onlyFiles,
} : undefined, {
type: 'switch',
text: i18n.ts.fileAttachedOnly,
- icon: 'ph-image ph-bold ph-lg',
- ref: $$(onlyFiles),
+ ref: onlyFiles,
+ disabled: src.value === 'local' || src.value === 'social' ? withReplies : false,
}], ev.currentTarget ?? ev.target);
},
},
@@ -181,14 +214,14 @@ const headerActions = $computed(() => {
text: i18n.ts.reload,
handler: (ev: Event) => {
console.log('called');
- tlComponent.reloadTimeline();
+ tlComponent.value.reloadTimeline();
},
});
}
return tmp;
});
-const headerTabs = $computed(() => [...(defaultStore.reactiveState.pinnedUserLists.value.map(l => ({
+const headerTabs = computed(() => [...(defaultStore.reactiveState.pinnedUserLists.value.map(l => ({
key: 'list:' + l.id,
title: l.name,
icon: 'ph-star ph-bold ph-lg',
@@ -235,7 +268,7 @@ const headerTabs = $computed(() => [...(defaultStore.reactiveState.pinnedUserLis
onClick: chooseChannel,
}] as Tab[]);
-const headerTabsWhenNotLogin = $computed(() => [
+const headerTabsWhenNotLogin = computed(() => [
...(isLocalTimelineAvailable ? [{
key: 'local',
title: i18n.ts._timelines.local,
@@ -252,7 +285,7 @@ const headerTabsWhenNotLogin = $computed(() => [
definePageMetadata(computed(() => ({
title: i18n.ts.timeline,
- icon: src === 'local' ? 'ph-planet ph-bold ph-lg' : src === 'social' ? 'ph-rocket-launch ph-bold ph-lg' : src === 'global' ? 'ph-globe-hemisphere-west ph-bold ph-lg' : 'ph-house ph-bold ph-lg',
+ icon: src.value === 'local' ? 'ph-planet ph-bold ph-lg' : src.value === 'social' ? 'ph-rocket-launch ph-bold ph-lg' : src.value === 'global' ? 'ph-globe-hemisphere-west ph-bold ph-lg' : src.value === 'bubble' ? 'ph-drop ph-bold ph-lg' : 'ph-house ph-bold ph-lg',
})));
</script>
diff --git a/packages/frontend/src/pages/user-list-timeline.vue b/packages/frontend/src/pages/user-list-timeline.vue
index e1cb7997e1..6d203a2882 100644
--- a/packages/frontend/src/pages/user-list-timeline.vue
+++ b/packages/frontend/src/pages/user-list-timeline.vue
@@ -24,7 +24,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { computed, watch } from 'vue';
+import { computed, watch, ref, shallowRef } from 'vue';
import MkTimeline from '@/components/MkTimeline.vue';
import { scroll } from '@/scripts/scroll.js';
import * as os from '@/os.js';
@@ -38,39 +38,39 @@ const props = defineProps<{
listId: string;
}>();
-let list = $ref(null);
-let queue = $ref(0);
-let tlEl = $shallowRef<InstanceType<typeof MkTimeline>>();
-let rootEl = $shallowRef<HTMLElement>();
+const list = ref(null);
+const queue = ref(0);
+const tlEl = shallowRef<InstanceType<typeof MkTimeline>>();
+const rootEl = shallowRef<HTMLElement>();
watch(() => props.listId, async () => {
- list = await os.api('users/lists/show', {
+ list.value = await os.api('users/lists/show', {
listId: props.listId,
});
}, { immediate: true });
function queueUpdated(q) {
- queue = q;
+ queue.value = q;
}
function top() {
- scroll(rootEl, { top: 0 });
+ scroll(rootEl.value, { top: 0 });
}
function settings() {
router.push(`/my/lists/${props.listId}`);
}
-const headerActions = $computed(() => list ? [{
+const headerActions = computed(() => list.value ? [{
icon: 'ph-gear ph-bold ph-lg',
text: i18n.ts.settings,
handler: settings,
}] : []);
-const headerTabs = $computed(() => []);
+const headerTabs = computed(() => []);
-definePageMetadata(computed(() => list ? {
- title: list.name,
+definePageMetadata(computed(() => list.value ? {
+ title: list.value.name,
icon: 'ph-list ph-bold ph-lg',
} : null));
</script>
diff --git a/packages/frontend/src/pages/user-tag.vue b/packages/frontend/src/pages/user-tag.vue
index 21ad7e7e5e..7e6757bba5 100644
--- a/packages/frontend/src/pages/user-tag.vue
+++ b/packages/frontend/src/pages/user-tag.vue
@@ -16,8 +16,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { computed, watch } from 'vue';
-import * as os from '@/os.js';
+import { computed } from 'vue';
import MkUserList from '@/components/MkUserList.vue';
import { definePageMetadata } from '@/scripts/page-metadata.js';
@@ -25,7 +24,7 @@ const props = defineProps<{
tag: string;
}>();
-const tagUsers = $computed(() => ({
+const tagUsers = computed(() => ({
endpoint: 'hashtags/users' as const,
limit: 30,
params: {
diff --git a/packages/frontend/src/pages/user/activity.following.vue b/packages/frontend/src/pages/user/activity.following.vue
index bc6bf77168..bd1159cb32 100644
--- a/packages/frontend/src/pages/user/activity.following.vue
+++ b/packages/frontend/src/pages/user/activity.following.vue
@@ -14,7 +14,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { onMounted } from 'vue';
+import { onMounted, shallowRef, ref } from 'vue';
import { Chart, ChartDataset } from 'chart.js';
import * as Misskey from 'misskey-js';
import gradient from 'chartjs-plugin-gradient';
@@ -32,12 +32,12 @@ const props = defineProps<{
user: Misskey.entities.User;
}>();
-const chartEl = $shallowRef<HTMLCanvasElement>(null);
-let legendEl = $shallowRef<InstanceType<typeof MkChartLegend>>();
+const chartEl = shallowRef<HTMLCanvasElement>(null);
+const legendEl = shallowRef<InstanceType<typeof MkChartLegend>>();
const now = new Date();
let chartInstance: Chart = null;
const chartLimit = 30;
-let fetching = $ref(true);
+const fetching = ref(true);
const { handler: externalTooltipHandler } = useChartTooltip();
@@ -88,7 +88,7 @@ async function renderChart() {
}, extra);
}
- chartInstance = new Chart(chartEl, {
+ chartInstance = new Chart(chartEl.value, {
type: 'bar',
data: {
datasets: [
@@ -162,10 +162,10 @@ async function renderChart() {
gradient,
},
},
- plugins: [chartVLine(vLineColor), chartLegend(legendEl)],
+ plugins: [chartVLine(vLineColor), chartLegend(legendEl.value)],
});
- fetching = false;
+ fetching.value = false;
}
onMounted(async () => {
diff --git a/packages/frontend/src/pages/user/activity.heatmap.vue b/packages/frontend/src/pages/user/activity.heatmap.vue
index 73bec2487e..ff46db9653 100644
--- a/packages/frontend/src/pages/user/activity.heatmap.vue
+++ b/packages/frontend/src/pages/user/activity.heatmap.vue
@@ -13,7 +13,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { onMounted, nextTick, watch } from 'vue';
+import { onMounted, nextTick, watch, shallowRef, ref } from 'vue';
import { Chart } from 'chart.js';
import * as Misskey from 'misskey-js';
import * as os from '@/os.js';
@@ -29,11 +29,11 @@ const props = defineProps<{
user: Misskey.entities.User;
}>();
-const rootEl = $shallowRef<HTMLDivElement>(null);
-const chartEl = $shallowRef<HTMLCanvasElement>(null);
+const rootEl = shallowRef<HTMLDivElement>(null);
+const chartEl = shallowRef<HTMLCanvasElement>(null);
const now = new Date();
let chartInstance: Chart = null;
-let fetching = $ref(true);
+const fetching = ref(true);
const { handler: externalTooltipHandler } = useChartTooltip({
position: 'middle',
@@ -44,8 +44,8 @@ async function renderChart() {
chartInstance.destroy();
}
- const wide = rootEl.offsetWidth > 700;
- const narrow = rootEl.offsetWidth < 400;
+ const wide = rootEl.value.offsetWidth > 700;
+ const narrow = rootEl.value.offsetWidth < 400;
const weeks = wide ? 50 : narrow ? 10 : 25;
const chartLimit = 7 * weeks;
@@ -78,7 +78,7 @@ async function renderChart() {
values = raw.inc;
}
- fetching = false;
+ fetching.value = false;
await nextTick();
@@ -91,7 +91,7 @@ async function renderChart() {
const marginEachCell = 4;
- chartInstance = new Chart(chartEl, {
+ chartInstance = new Chart(chartEl.value, {
type: 'matrix',
data: {
datasets: [{
@@ -203,7 +203,7 @@ async function renderChart() {
}
watch(() => props.src, () => {
- fetching = true;
+ fetching.value = true;
renderChart();
});
diff --git a/packages/frontend/src/pages/user/activity.notes.vue b/packages/frontend/src/pages/user/activity.notes.vue
index 5ba4af7ca1..dd035641d8 100644
--- a/packages/frontend/src/pages/user/activity.notes.vue
+++ b/packages/frontend/src/pages/user/activity.notes.vue
@@ -14,7 +14,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { onMounted } from 'vue';
+import { onMounted, shallowRef, ref } from 'vue';
import { Chart, ChartDataset } from 'chart.js';
import * as Misskey from 'misskey-js';
import gradient from 'chartjs-plugin-gradient';
@@ -32,12 +32,12 @@ const props = defineProps<{
user: Misskey.entities.User;
}>();
-const chartEl = $shallowRef<HTMLCanvasElement>(null);
-let legendEl = $shallowRef<InstanceType<typeof MkChartLegend>>();
+const chartEl = shallowRef<HTMLCanvasElement>(null);
+const legendEl = shallowRef<InstanceType<typeof MkChartLegend>>();
const now = new Date();
let chartInstance: Chart = null;
const chartLimit = 50;
-let fetching = $ref(true);
+const fetching = ref(true);
const { handler: externalTooltipHandler } = useChartTooltip();
@@ -87,7 +87,7 @@ async function renderChart() {
}, extra);
}
- chartInstance = new Chart(chartEl, {
+ chartInstance = new Chart(chartEl.value, {
type: 'bar',
data: {
datasets: [
@@ -161,10 +161,10 @@ async function renderChart() {
gradient,
},
},
- plugins: [chartVLine(vLineColor), chartLegend(legendEl)],
+ plugins: [chartVLine(vLineColor), chartLegend(legendEl.value)],
});
- fetching = false;
+ fetching.value = false;
}
onMounted(async () => {
diff --git a/packages/frontend/src/pages/user/activity.pv.vue b/packages/frontend/src/pages/user/activity.pv.vue
index 54d1d0c1be..2dd9a1570f 100644
--- a/packages/frontend/src/pages/user/activity.pv.vue
+++ b/packages/frontend/src/pages/user/activity.pv.vue
@@ -14,7 +14,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { onMounted } from 'vue';
+import { onMounted, shallowRef, ref } from 'vue';
import { Chart, ChartDataset } from 'chart.js';
import * as Misskey from 'misskey-js';
import gradient from 'chartjs-plugin-gradient';
@@ -32,12 +32,12 @@ const props = defineProps<{
user: Misskey.entities.User;
}>();
-const chartEl = $shallowRef<HTMLCanvasElement>(null);
-let legendEl = $shallowRef<InstanceType<typeof MkChartLegend>>();
+const chartEl = shallowRef<HTMLCanvasElement>(null);
+const legendEl = shallowRef<InstanceType<typeof MkChartLegend>>();
const now = new Date();
let chartInstance: Chart = null;
const chartLimit = 30;
-let fetching = $ref(true);
+const fetching = ref(true);
const { handler: externalTooltipHandler } = useChartTooltip();
@@ -88,7 +88,7 @@ async function renderChart() {
}, extra);
}
- chartInstance = new Chart(chartEl, {
+ chartInstance = new Chart(chartEl.value, {
type: 'bar',
data: {
datasets: [
@@ -172,10 +172,10 @@ async function renderChart() {
gradient,
},
},
- plugins: [chartVLine(vLineColor), chartLegend(legendEl)],
+ plugins: [chartVLine(vLineColor), chartLegend(legendEl.value)],
});
- fetching = false;
+ fetching.value = false;
}
onMounted(async () => {
diff --git a/packages/frontend/src/pages/user/followers.vue b/packages/frontend/src/pages/user/followers.vue
index b41edc74a1..5da950e853 100644
--- a/packages/frontend/src/pages/user/followers.vue
+++ b/packages/frontend/src/pages/user/followers.vue
@@ -19,7 +19,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { computed, watch } from 'vue';
+import { computed, watch, ref } from 'vue';
import * as Misskey from 'misskey-js';
import XFollowList from './follow-list.vue';
import * as os from '@/os.js';
@@ -31,16 +31,16 @@ const props = withDefaults(defineProps<{
}>(), {
});
-let user = $ref<null | Misskey.entities.UserDetailed>(null);
-let error = $ref(null);
+const user = ref<null | Misskey.entities.UserDetailed>(null);
+const error = ref(null);
function fetchUser(): void {
if (props.acct == null) return;
- user = null;
+ user.value = null;
os.api('users/show', Misskey.acct.parse(props.acct)).then(u => {
- user = u;
+ user.value = u;
}).catch(err => {
- error = err;
+ error.value = err;
});
}
@@ -48,15 +48,15 @@ watch(() => props.acct, fetchUser, {
immediate: true,
});
-const headerActions = $computed(() => []);
+const headerActions = computed(() => []);
-const headerTabs = $computed(() => []);
+const headerTabs = computed(() => []);
-definePageMetadata(computed(() => user ? {
+definePageMetadata(computed(() => user.value ? {
icon: 'ph-user ph-bold ph-lg',
- title: user.name ? `${user.name} (@${user.username})` : `@${user.username}`,
+ title: user.value.name ? `${user.value.name} (@${user.value.username})` : `@${user.value.username}`,
subtitle: i18n.ts.followers,
- userName: user,
- avatar: user,
+ userName: user.value,
+ avatar: user.value,
} : null));
</script>
diff --git a/packages/frontend/src/pages/user/following.vue b/packages/frontend/src/pages/user/following.vue
index ba5ddafbdb..6ba134cd78 100644
--- a/packages/frontend/src/pages/user/following.vue
+++ b/packages/frontend/src/pages/user/following.vue
@@ -19,7 +19,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { computed, watch } from 'vue';
+import { computed, watch, ref } from 'vue';
import * as Misskey from 'misskey-js';
import XFollowList from './follow-list.vue';
import * as os from '@/os.js';
@@ -31,16 +31,16 @@ const props = withDefaults(defineProps<{
}>(), {
});
-let user = $ref<null | Misskey.entities.UserDetailed>(null);
-let error = $ref(null);
+const user = ref<null | Misskey.entities.UserDetailed>(null);
+const error = ref(null);
function fetchUser(): void {
if (props.acct == null) return;
- user = null;
+ user.value = null;
os.api('users/show', Misskey.acct.parse(props.acct)).then(u => {
- user = u;
+ user.value = u;
}).catch(err => {
- error = err;
+ error.value = err;
});
}
@@ -48,15 +48,15 @@ watch(() => props.acct, fetchUser, {
immediate: true,
});
-const headerActions = $computed(() => []);
+const headerActions = computed(() => []);
-const headerTabs = $computed(() => []);
+const headerTabs = computed(() => []);
-definePageMetadata(computed(() => user ? {
+definePageMetadata(computed(() => user.value ? {
icon: 'ph-user ph-bold ph-lg',
- title: user.name ? `${user.name} (@${user.username})` : `@${user.username}`,
+ title: user.value.name ? `${user.value.name} (@${user.value.username})` : `@${user.value.username}`,
subtitle: i18n.ts.following,
- userName: user,
- avatar: user,
+ userName: user.value,
+ avatar: user.value,
} : null));
</script>
diff --git a/packages/frontend/src/pages/user/home.vue b/packages/frontend/src/pages/user/home.vue
index 2e7fc68748..44a8ca250b 100644
--- a/packages/frontend/src/pages/user/home.vue
+++ b/packages/frontend/src/pages/user/home.vue
@@ -107,11 +107,11 @@ SPDX-License-Identifier: AGPL-3.0-only
<b>{{ number(user.notesCount) }}</b>
<span>{{ i18n.ts.notes }}</span>
</MkA>
- <MkA v-if="isFfVisibleForMe(user)" :to="userPage(user, 'following')">
+ <MkA v-if="isFollowingVisibleForMe(user)" :to="userPage(user, 'following')">
<b>{{ number(user.followingCount) }}</b>
<span>{{ i18n.ts.following }}</span>
</MkA>
- <MkA v-if="isFfVisibleForMe(user)" :to="userPage(user, 'followers')">
+ <MkA v-if="isFollowersVisibleForMe(user)" :to="userPage(user, 'followers')">
<b>{{ number(user.followersCount) }}</b>
<span>{{ i18n.ts.followers }}</span>
</MkA>
@@ -128,13 +128,20 @@ SPDX-License-Identifier: AGPL-3.0-only
</div>
<MkInfo v-else-if="$i && $i.id === user.id">{{ i18n.ts.userPagePinTip }}</MkInfo>
<template v-if="narrow">
- <XFiles :key="user.id" :user="user"/>
- <XActivity :key="user.id" :user="user"/>
- <XListenBrainz v-if="user.listenbrainz && listenbrainzdata" :key="user.id" :user="user"/>
+ <MkLazy>
+ <XFiles :key="user.id" :user="user"/>
+ </MkLazy>
+ <MkLazy>
+ <XActivity :key="user.id" :user="user"/>
+ </MkLazy>
+ <MkLazy>
+ <XListenBrainz v-if="user.listenbrainz && listenbrainzdata" :key="user.id" :user="user"/>
+ </MkLazy>
</template>
<!-- <div v-if="!disableNotes">
- <div style="margin-bottom: 8px; z-index: 1;">{{ i18n.ts.featured }}</div>
- <MkNotes :class="$style.tl" :noGap="true" :pagination="pagination"/>
+ <MkLazy>
+ <XTimeline :user="user"/>
+ </MkLazy>
</div> -->
<MkStickyContainer>
<template #header>
@@ -144,7 +151,9 @@ SPDX-License-Identifier: AGPL-3.0-only
<option value="files">{{ i18n.ts.withFiles }}</option>
</MkTab>
</template>
- <MkNotes :class="$style.tl" :noGap="true" :pagination="AllPagination"/>
+ <MkLazy>
+ <MkNotes :class="$style.tl" :noGap="true" :pagination="AllPagination"/>
+ </MkLazy>
</MkStickyContainer>
</div>
</div>
@@ -159,10 +168,11 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { defineAsyncComponent, computed, onMounted, onUnmounted, nextTick, watch } from 'vue';
+import { defineAsyncComponent, computed, onMounted, onUnmounted, nextTick, watch, ref } from 'vue';
import * as Misskey from 'misskey-js';
import MkTab from '@/components/MkTab.vue';
import MkNote from '@/components/MkNote.vue';
+import MkNotes from '@/components/MkNotes.vue';
import SkNote from '@/components/SkNote.vue';
import MkFollowButton from '@/components/MkFollowButton.vue';
import MkAccountMoved from '@/components/MkAccountMoved.vue';
@@ -180,10 +190,9 @@ import { i18n } from '@/i18n.js';
import { $i, iAmModerator } from '@/account.js';
import { dateString } from '@/filters/date.js';
import { confetti } from '@/scripts/confetti.js';
-import MkNotes from '@/components/MkNotes.vue';
import { api } from '@/os.js';
import { defaultStore } from '@/store.js';
-import { isFfVisibleForMe } from '@/scripts/isFfVisibleForMe.js';
+import { isFollowingVisibleForMe, isFollowersVisibleForMe } from '@/scripts/isFfVisibleForMe.js';
function calcAge(birthdate: string): number {
const date = new Date(birthdate);
@@ -203,6 +212,7 @@ function calcAge(birthdate: string): number {
const XFiles = defineAsyncComponent(() => import('./index.files.vue'));
const XActivity = defineAsyncComponent(() => import('./index.activity.vue'));
const XListenBrainz = defineAsyncComponent(() => import("./index.listenbrainz.vue"));
+//const XTimeline = defineAsyncComponent(() => import('./index.timeline.vue'));
const props = withDefaults(defineProps<{
user: Misskey.entities.UserDetailed;
@@ -214,17 +224,17 @@ const props = withDefaults(defineProps<{
const router = useRouter();
-let user = $ref(props.user);
-let parallaxAnimationId = $ref<null | number>(null);
-let narrow = $ref<null | boolean>(null);
-let rootEl = $ref<null | HTMLElement>(null);
-let bannerEl = $ref<null | HTMLElement>(null);
-let memoTextareaEl = $ref<null | HTMLElement>(null);
-let memoDraft = $ref(props.user.memo);
-let isEditingMemo = $ref(false);
-let moderationNote = $ref(props.user.moderationNote);
-let editModerationNote = $ref(false);
-let noteview = $ref<string | null>(null);
+const user = ref(props.user);
+const parallaxAnimationId = ref<null | number>(null);
+const narrow = ref<null | boolean>(null);
+const rootEl = ref<null | HTMLElement>(null);
+const bannerEl = ref<null | HTMLElement>(null);
+const memoTextareaEl = ref<null | HTMLElement>(null);
+const memoDraft = ref(props.user.memo);
+const isEditingMemo = ref(false);
+const moderationNote = ref(props.user.moderationNote);
+const editModerationNote = ref(false);
+const noteview = ref<string | null>(null);
let listenbrainzdata = false;
if (props.user.listenbrainz) {
@@ -251,8 +261,8 @@ const background = computed(() => {
};
});
-watch($$(moderationNote), async () => {
- await os.api('admin/update-user-note', { userId: props.user.id, text: moderationNote });
+watch(moderationNote, async () => {
+ await os.api('admin/update-user-note', { userId: props.user.id, text: moderationNote.value });
});
const pagination = {
@@ -268,39 +278,39 @@ const AllPagination = {
limit: 10,
params: computed(() => ({
userId: props.user.id,
- withRenotes: noteview === 'all',
- withReplies: noteview === 'all' || noteview === 'files',
- withChannelNotes: noteview === 'all',
- withFiles: noteview === 'files',
+ withRenotes: noteview.value === 'all',
+ withReplies: noteview.value === 'all' || noteview.value === 'files',
+ withChannelNotes: noteview.value === 'all',
+ withFiles: noteview.value === 'files',
})),
};
-const style = $computed(() => {
+const style = computed(() => {
if (props.user.bannerUrl == null) return {};
return {
backgroundImage: `url(${ props.user.bannerUrl })`,
};
});
-const age = $computed(() => {
+const age = computed(() => {
return calcAge(props.user.birthday);
});
-function menu(ev) {
- const { menu, cleanup } = getUserMenu(user, router);
+function menu(ev: MouseEvent) {
+ const { menu, cleanup } = getUserMenu(user.value, router);
os.popupMenu(menu, ev.currentTarget ?? ev.target).finally(cleanup);
}
function parallaxLoop() {
- parallaxAnimationId = window.requestAnimationFrame(parallaxLoop);
+ parallaxAnimationId.value = window.requestAnimationFrame(parallaxLoop);
parallax();
}
function parallax() {
- const banner = bannerEl as any;
+ const banner = bannerEl.value as any;
if (banner == null) return;
- const top = getScrollPosition(rootEl);
+ const top = getScrollPosition(rootEl.value);
if (top < 0) return;
@@ -310,33 +320,33 @@ function parallax() {
}
function showMemoTextarea() {
- isEditingMemo = true;
+ isEditingMemo.value = true;
nextTick(() => {
- memoTextareaEl?.focus();
+ memoTextareaEl.value?.focus();
});
}
function adjustMemoTextarea() {
- if (!memoTextareaEl) return;
- memoTextareaEl.style.height = '0px';
- memoTextareaEl.style.height = `${memoTextareaEl.scrollHeight}px`;
+ if (!memoTextareaEl.value) return;
+ memoTextareaEl.value.style.height = '0px';
+ memoTextareaEl.value.style.height = `${memoTextareaEl.value.scrollHeight}px`;
}
async function updateMemo() {
await api('users/update-memo', {
- memo: memoDraft,
+ memo: memoDraft.value,
userId: props.user.id,
});
- isEditingMemo = false;
+ isEditingMemo.value = false;
}
watch([props.user], () => {
- memoDraft = props.user.memo;
+ memoDraft.value = props.user.memo;
});
onMounted(() => {
window.requestAnimationFrame(parallaxLoop);
- narrow = rootEl!.clientWidth < 1000;
+ narrow.value = rootEl.value!.clientWidth < 1000;
if (props.user.birthday) {
const m = new Date().getMonth() + 1;
@@ -355,8 +365,8 @@ onMounted(() => {
});
onUnmounted(() => {
- if (parallaxAnimationId) {
- window.cancelAnimationFrame(parallaxAnimationId);
+ if (parallaxAnimationId.value) {
+ window.cancelAnimationFrame(parallaxAnimationId.value);
}
});
</script>
diff --git a/packages/frontend/src/pages/user/index.activity.vue b/packages/frontend/src/pages/user/index.activity.vue
index b7576d4e20..f555486a6d 100644
--- a/packages/frontend/src/pages/user/index.activity.vue
+++ b/packages/frontend/src/pages/user/index.activity.vue
@@ -20,7 +20,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { } from 'vue';
+import { ref } from 'vue';
import * as Misskey from 'misskey-js';
import MkContainer from '@/components/MkContainer.vue';
import MkChart from '@/components/MkChart.vue';
@@ -34,20 +34,20 @@ const props = withDefaults(defineProps<{
limit: 50,
});
-let chartSrc = $ref('per-user-notes');
+const chartSrc = ref('per-user-notes');
function showMenu(ev: MouseEvent) {
os.popupMenu([{
text: i18n.ts.notes,
- active: chartSrc === 'per-user-notes',
+ active: chartSrc.value === 'per-user-notes',
action: () => {
- chartSrc = 'per-user-notes';
+ chartSrc.value = 'per-user-notes';
},
}, {
text: i18n.ts.numberOfProfileView,
- active: chartSrc === 'per-user-pv',
+ active: chartSrc.value === 'per-user-pv',
action: () => {
- chartSrc = 'per-user-pv';
+ chartSrc.value = 'per-user-pv';
},
}, /*, {
text: i18n.ts.following,
diff --git a/packages/frontend/src/pages/user/index.files.vue b/packages/frontend/src/pages/user/index.files.vue
index d6d90c46e0..30817db77c 100644
--- a/packages/frontend/src/pages/user/index.files.vue
+++ b/packages/frontend/src/pages/user/index.files.vue
@@ -11,10 +11,14 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkLoading v-if="fetching"/>
<div v-if="!fetching && files.length > 0" :class="$style.stream">
<template v-for="file in files" :key="file.note.id + file.file.id">
- <div v-if="file.file.isSensitive && !showingFiles.includes(file.file.id)" :class="$style.sensitive" @click="showingFiles.push(file.file.id)">
- <div>
- <div><i class="ph-eye-slash ph-bold ph-lg"></i> {{ i18n.ts.sensitive }}</div>
- <div>{{ i18n.ts.clickToShow }}</div>
+ <div v-if="file.file.isSensitive && !showingFiles.includes(file.file.id)" :class="$style.img" @click="showingFiles.push(file.file.id)">
+ <!-- TODO: 画像以外のファイルに対応 -->
+ <ImgWithBlurhash :class="$style.sensitiveImg" :hash="file.file.blurhash" :src="thumbnail(file.file)" :title="file.file.name" :forceBlurhash="true"/>
+ <div :class="$style.sensitive">
+ <div>
+ <div><i class="ph-eye-slash ph-bold ph-lg"></i> {{ i18n.ts.sensitive }}</div>
+ <div>{{ i18n.ts.clickToShow }}</div>
+ </div>
</div>
</div>
<MkA v-else :class="$style.img" :to="notePage(file.note)">
@@ -29,7 +33,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { onMounted } from 'vue';
+import { onMounted, ref } from 'vue';
import * as Misskey from 'misskey-js';
import { getStaticImageUrl } from '@/scripts/media-proxy.js';
import { notePage } from '@/filters/note.js';
@@ -43,12 +47,12 @@ const props = defineProps<{
user: Misskey.entities.UserDetailed;
}>();
-let fetching = $ref(true);
-let files = $ref<{
+const fetching = ref(true);
+const files = ref<{
note: Misskey.entities.Note;
file: Misskey.entities.DriveFile;
}[]>([]);
-let showingFiles = $ref<string[]>([]);
+const showingFiles = ref<string[]>([]);
function thumbnail(image: Misskey.entities.DriveFile): string {
return defaultStore.state.disableShowingAnimatedImages
@@ -60,18 +64,17 @@ onMounted(() => {
os.api('users/notes', {
userId: props.user.id,
withFiles: true,
- excludeNsfw: defaultStore.state.nsfw !== 'ignore',
limit: 15,
}).then(notes => {
for (const note of notes) {
for (const file of note.files) {
- files.push({
+ files.value.push({
note,
file,
});
}
}
- fetching = false;
+ fetching.value = false;
});
});
</script>
@@ -88,6 +91,7 @@ onMounted(() => {
}
.img {
+ position: relative;
height: 128px;
border-radius: var(--radius-sm);
overflow: clip;
@@ -99,8 +103,24 @@ onMounted(() => {
text-align: center;
}
+.sensitiveImg {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ filter: brightness(0.7);
+}
.sensitive {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
display: grid;
place-items: center;
+ font-size: 0.8em;
+ color: #fff;
+ cursor: pointer;
}
</style>
diff --git a/packages/frontend/src/pages/user/index.timeline.vue b/packages/frontend/src/pages/user/index.timeline.vue
index 6cf5bcf91f..e5a0f49e3d 100644
--- a/packages/frontend/src/pages/user/index.timeline.vue
+++ b/packages/frontend/src/pages/user/index.timeline.vue
@@ -4,18 +4,17 @@ SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
-<MkSpacer :contentMax="800" style="padding-top: 0">
- <MkStickyContainer>
- <template #header>
- <MkTab v-model="include" :class="$style.tab">
- <option :value="null">{{ i18n.ts.notes }}</option>
- <option value="all">{{ i18n.ts.all }}</option>
- <option value="files">{{ i18n.ts.withFiles }}</option>
- </MkTab>
- </template>
- <MkNotes :noGap="true" :pagination="pagination" :class="$style.tl"/>
- </MkStickyContainer>
-</MkSpacer>
+<MkStickyContainer>
+ <template #header>
+ <MkTab v-model="tab" :class="$style.tab">
+ <option value="featured">{{ i18n.ts.featured }}</option>
+ <option :value="null">{{ i18n.ts.notes }}</option>
+ <option value="all">{{ i18n.ts.all }}</option>
+ <option value="files">{{ i18n.ts.withFiles }}</option>
+ </MkTab>
+ </template>
+ <MkNotes :noGap="true" :pagination="pagination" :class="$style.tl"/>
+</MkStickyContainer>
</template>
<script lang="ts" setup>
@@ -29,24 +28,29 @@ const props = defineProps<{
user: Misskey.entities.UserDetailed;
}>();
-const include = ref<string | null>('all');
+const tab = ref<string | null>('all');
-const pagination = {
+const pagination = computed(() => tab.value === 'featured' ? {
+ endpoint: 'users/featured-notes' as const,
+ limit: 10,
+ params: {
+ userId: props.user.id,
+ },
+} : {
endpoint: 'users/notes' as const,
limit: 10,
- params: computed(() => ({
+ params: {
userId: props.user.id,
- withRenotes: include.value === 'all',
- withReplies: include.value === 'all',
- withChannelNotes: include.value === 'all',
- withFiles: include.value === 'files',
- })),
-};
+ withRenotes: tab.value === 'all',
+ withReplies: tab.value === 'all',
+ withChannelNotes: tab.value === 'all',
+ withFiles: tab.value === 'files',
+ },
+});
</script>
<style lang="scss" module>
.tab {
- margin: calc(var(--margin) / 2) 0;
padding: calc(var(--margin) / 2) 0;
background: var(--bg);
}
diff --git a/packages/frontend/src/pages/user/index.vue b/packages/frontend/src/pages/user/index.vue
index df07dd9786..c0064d2503 100644
--- a/packages/frontend/src/pages/user/index.vue
+++ b/packages/frontend/src/pages/user/index.vue
@@ -9,7 +9,9 @@ SPDX-License-Identifier: AGPL-3.0-only
<div>
<div v-if="user">
<XHome v-if="tab === 'home'" :user="user"/>
- <XTimeline v-else-if="tab === 'notes'" :user="user"/>
+ <MkSpacer v-else-if="tab === 'notes'" :contentMax="800" style="padding-top: 0">
+ <XTimeline :user="user"/>
+ </MkSpacer>
<XActivity v-else-if="tab === 'activity'" :user="user"/>
<XAchievements v-else-if="tab === 'achievements'" :user="user"/>
<XReactions v-else-if="tab === 'reactions'" :user="user"/>
@@ -18,6 +20,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<XPages v-else-if="tab === 'pages'" :user="user"/>
<XFlashs v-else-if="tab === 'flashs'" :user="user"/>
<XGallery v-else-if="tab === 'gallery'" :user="user"/>
+ <XRaw v-else-if="tab === 'raw'" :user="user"/>
</div>
<MkError v-else-if="error" @retry="fetchUser()"/>
<MkLoading v-else/>
@@ -26,7 +29,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { defineAsyncComponent, computed, watch } from 'vue';
+import { defineAsyncComponent, computed, watch, ref } from 'vue';
import * as Misskey from 'misskey-js';
import { acct as getAcct } from '@/filters/user.js';
import * as os from '@/os.js';
@@ -44,6 +47,7 @@ const XLists = defineAsyncComponent(() => import('./lists.vue'));
const XPages = defineAsyncComponent(() => import('./pages.vue'));
const XFlashs = defineAsyncComponent(() => import('./flashs.vue'));
const XGallery = defineAsyncComponent(() => import('./gallery.vue'));
+const XRaw = defineAsyncComponent(() => import('./raw.vue'));
const props = withDefaults(defineProps<{
acct: string;
@@ -52,17 +56,17 @@ const props = withDefaults(defineProps<{
page: 'home',
});
-let tab = $ref(props.page);
-let user = $ref<null | Misskey.entities.UserDetailed>(null);
-let error = $ref(null);
+const tab = ref(props.page);
+const user = ref<null | Misskey.entities.UserDetailed>(null);
+const error = ref(null);
function fetchUser(): void {
if (props.acct == null) return;
- user = null;
+ user.value = null;
os.api('users/show', Misskey.acct.parse(props.acct)).then(u => {
- user = u;
+ user.value = u;
}).catch(err => {
- error = err;
+ error.value = err;
});
}
@@ -70,9 +74,9 @@ watch(() => props.acct, fetchUser, {
immediate: true,
});
-const headerActions = $computed(() => []);
+const headerActions = computed(() => []);
-const headerTabs = $computed(() => user ? [{
+const headerTabs = computed(() => user.value ? [{
key: 'home',
title: i18n.ts.overview,
icon: 'ph-house ph-bold ph-lg',
@@ -84,11 +88,11 @@ const headerTabs = $computed(() => user ? [{
key: 'activity',
title: i18n.ts.activity,
icon: 'ph-chart-line ph-bold ph-lg',
-}, ...(user.host == null ? [{
+}, ...(user.value.host == null ? [{
key: 'achievements',
title: i18n.ts.achievements,
icon: 'ph-trophy ph-bold ph-lg',
-}] : []), ...($i && ($i.id === user.id)) || user.publicReactions ? [{
+}] : []), ...($i && ($i.id === user.value.id)) || user.value.publicReactions ? [{
key: 'reactions',
title: i18n.ts.reaction,
icon: 'ph-smiley ph-bold ph-lg',
@@ -112,17 +116,21 @@ const headerTabs = $computed(() => user ? [{
key: 'gallery',
title: i18n.ts.gallery,
icon: 'ph-images-square ph-bold ph-lg',
+}, {
+ key: 'raw',
+ title: 'Raw',
+ icon: 'ph-code ph-bold ph-lg',
}] : []);
-definePageMetadata(computed(() => user ? {
+definePageMetadata(computed(() => user.value ? {
icon: 'ph-user ph-bold ph-lg',
- title: user.name ? `${user.name} (@${user.username})` : `@${user.username}`,
- subtitle: `@${getAcct(user)}`,
- userName: user,
- avatar: user,
- path: `/@${user.username}`,
+ title: user.value.name ? `${user.value.name} (@${user.value.username})` : `@${user.value.username}`,
+ subtitle: `@${getAcct(user.value)}`,
+ userName: user.value,
+ avatar: user.value,
+ path: `/@${user.value.username}`,
share: {
- title: user.name,
+ title: user.value.name,
},
} : null));
</script>
diff --git a/packages/frontend/src/pages/user/raw.vue b/packages/frontend/src/pages/user/raw.vue
new file mode 100644
index 0000000000..0c0bfc29ca
--- /dev/null
+++ b/packages/frontend/src/pages/user/raw.vue
@@ -0,0 +1,130 @@
+<!--
+SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-License-Identifier: AGPL-3.0-only
+-->
+
+<template>
+<MkSpacer :contentMax="600" :marginMin="16" :marginMax="32">
+ <div class="_gaps_m">
+ <div :class="$style.userMInfoRoot">
+ <MkAvatar :class="$style.userMInfoAvatar" :user="user" indicator link preview/>
+ <div :class="$style.userMInfoMetaRoot">
+ <span :class="$style.userMInfoMetaName"><MkUserName :class="$style.userMInfoMetaName" :user="user"/></span>
+ <span :class="$style.userMInfoMetaSub"><span class="acct _monospace">@{{ acct(user) }}</span></span>
+ <span :class="$style.userMInfoMetaState">
+ <span v-if="suspended" :class="$style.suspended">Suspended</span>
+ <span v-if="silenced" :class="$style.silenced">Silenced</span>
+ <span v-if="moderator" :class="$style.moderator">Moderator</span>
+ </span>
+ </div>
+ </div>
+
+ <div style="display: flex; flex-direction: column; gap: 1em;">
+ <MkKeyValue :copy="user.id" oneline>
+ <template #key>ID</template>
+ <template #value><span class="_monospace">{{ user.id }}</span></template>
+ </MkKeyValue>
+ <MkKeyValue oneline>
+ <template #key>{{ i18n.ts.createdAt }}</template>
+ <template #value><span class="_monospace"><MkTime :time="user.createdAt" :mode="'detail'"/></span></template>
+ </MkKeyValue>
+ </div>
+
+ <FormSection>
+ <template #label>Raw</template>
+ <MkObjectView tall :value="user"></MkObjectView>
+ </FormSection>
+ </div>
+</MkSpacer>
+</template>
+
+<script lang="ts" setup>
+import { computed } from 'vue';
+import * as Misskey from 'misskey-js';
+import { acct } from '@/filters/user.js';
+import { i18n } from '@/i18n.js';
+import MkKeyValue from '@/components/MkKeyValue.vue';
+import FormSection from '@/components/form/section.vue';
+import MkObjectView from '@/components/MkObjectView.vue';
+
+const props = defineProps<{
+ user: Misskey.entities.User;
+}>();
+
+const moderator = computed(() => props.user.isModerator ?? false);
+const silenced = computed(() => props.user.isSilenced ?? false);
+const suspended = computed(() => props.user.isSuspended ?? false);
+</script>
+
+<style lang="scss" module>
+.userMInfoRoot {
+ display: flex;
+ align-items: center;
+}
+
+.userMInfoAvatar {
+ display: block;
+ width: 64px;
+ height: 64px;
+ margin-right: 16px;
+}
+
+.userMInfoMetaRoot {
+ flex: 1;
+ overflow: hidden;
+}
+
+.userMInfoMetaName {
+ display: block;
+ width: 100%;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+.userMInfoMetaSub {
+ display: block;
+ width: 100%;
+ font-size: 85%;
+ opacity: 0.7;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+.userMInfoMetaState {
+ display: flex;
+ gap: 8px;
+ flex-wrap: wrap;
+ margin-top: 4px;
+
+ &:empty {
+ display: none;
+ }
+
+ > .suspended,
+ > .silenced,
+ > .moderator {
+ display: inline-block;
+ border: solid 1px;
+ border-radius: 6px;
+ padding: 2px 6px;
+ font-size: 85%;
+ }
+
+ > .suspended {
+ color: var(--error);
+ border-color: var(--error);
+ }
+
+ > .silenced {
+ color: var(--warn);
+ border-color: var(--warn);
+ }
+
+ > .moderator {
+ color: var(--success);
+ border-color: var(--success);
+ }
+}
+</style>
diff --git a/packages/frontend/src/pages/welcome.entrance.a.vue b/packages/frontend/src/pages/welcome.entrance.a.vue
index 52949c294c..50f86a0ae2 100644
--- a/packages/frontend/src/pages/welcome.entrance.a.vue
+++ b/packages/frontend/src/pages/welcome.entrance.a.vue
@@ -9,7 +9,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<XTimeline class="tl"/>
<div class="shape1"></div>
<div class="shape2"></div>
- <img src="/client-assets/sharkey.svg" class="misskey"/>
+ <img :src="misskeysvg" class="misskey"/>
<div class="emojis">
<MkEmoji :normal="true" :noStyle="true" emoji="👍"/>
<MkEmoji :normal="true" :noStyle="true" emoji="❤"/>
@@ -33,37 +33,35 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { } from 'vue';
+import { ref } from 'vue';
import * as Misskey from 'misskey-js';
import XTimeline from './welcome.timeline.vue';
import MarqueeText from '@/components/MkMarquee.vue';
import MkFeaturedPhotos from '@/components/MkFeaturedPhotos.vue';
-import MkInfo from '@/components/MkInfo.vue';
-import { instanceName } from '@/config.js';
+import misskeysvg from '/client-assets/sharkey.svg';
import * as os from '@/os.js';
-import { i18n } from '@/i18n.js';
-import { instance } from '@/instance.js';
-import number from '@/filters/number.js';
-import MkNumber from '@/components/MkNumber.vue';
import MkVisitorDashboard from '@/components/MkVisitorDashboard.vue';
import { getProxiedImageUrl } from '@/scripts/media-proxy.js';
-let meta = $ref<Misskey.entities.Instance>();
-let instances = $ref<any[]>();
+const meta = ref<Misskey.entities.MetaResponse>();
+const instances = ref<Misskey.entities.FederationInstance[]>();
-function getInstanceIcon(instance): string {
- return getProxiedImageUrl(instance.iconUrl, 'preview');
+function getInstanceIcon(instance: Misskey.entities.FederationInstance): string {
+ if (!instance.iconUrl) {
+ return '';
+ }
+ return getProxiedImageUrl(instance.iconUrl, 'preview');
}
os.api('meta', { detail: true }).then(_meta => {
- meta = _meta;
+ meta.value = _meta;
});
os.apiGet('federation/instances', {
sort: '+pubSub',
limit: 20,
}).then(_instances => {
- instances = _instances;
+ instances.value = _instances;
});
</script>
diff --git a/packages/frontend/src/pages/welcome.setup.vue b/packages/frontend/src/pages/welcome.setup.vue
index bac6cc1f0c..c2f9d4e585 100644
--- a/packages/frontend/src/pages/welcome.setup.vue
+++ b/packages/frontend/src/pages/welcome.setup.vue
@@ -35,7 +35,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { } from 'vue';
+import { ref } from 'vue';
import MkButton from '@/components/MkButton.vue';
import MkInput from '@/components/MkInput.vue';
import { host, version } from '@/config.js';
@@ -44,21 +44,21 @@ import { login } from '@/account.js';
import { i18n } from '@/i18n.js';
import MkAnimBg from '@/components/MkAnimBg.vue';
-let username = $ref('');
-let password = $ref('');
-let submitting = $ref(false);
+const username = ref('');
+const password = ref('');
+const submitting = ref(false);
function submit() {
- if (submitting) return;
- submitting = true;
+ if (submitting.value) return;
+ submitting.value = true;
os.api('admin/accounts/create', {
- username: username,
- password: password,
+ username: username.value,
+ password: password.value,
}).then(res => {
return login(res.token);
}).catch(() => {
- submitting = false;
+ submitting.value = false;
os.alert({
type: 'error',
diff --git a/packages/frontend/src/pages/welcome.timeline.vue b/packages/frontend/src/pages/welcome.timeline.vue
index 6c13af2f0a..2cbe0ed9b1 100644
--- a/packages/frontend/src/pages/welcome.timeline.vue
+++ b/packages/frontend/src/pages/welcome.timeline.vue
@@ -28,28 +28,27 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import * as Misskey from 'misskey-js';
-import { onUpdated } from 'vue';
+import { onUpdated, ref, shallowRef } from 'vue';
import MkReactionsViewer from '@/components/MkReactionsViewer.vue';
import MkMediaList from '@/components/MkMediaList.vue';
import MkPoll from '@/components/MkPoll.vue';
import * as os from '@/os.js';
import { getScrollContainer } from '@/scripts/scroll.js';
-import { $i } from '@/account.js';
-let notes = $ref<Misskey.entities.Note[]>([]);
-let isScrolling = $ref(false);
-let scrollEl = $shallowRef<HTMLElement>();
+const notes = ref<Misskey.entities.Note[]>([]);
+const isScrolling = ref(false);
+const scrollEl = shallowRef<HTMLElement>();
os.apiGet('notes/featured').then(_notes => {
- notes = _notes;
+ notes.value = _notes;
});
onUpdated(() => {
- if (!scrollEl) return;
- const container = getScrollContainer(scrollEl);
+ if (!scrollEl.value) return;
+ const container = getScrollContainer(scrollEl.value);
const containerHeight = container ? container.clientHeight : window.innerHeight;
- if (scrollEl.offsetHeight > containerHeight) {
- isScrolling = true;
+ if (scrollEl.value.offsetHeight > containerHeight) {
+ isScrolling.value = true;
}
});
</script>
diff --git a/packages/frontend/src/pages/welcome.vue b/packages/frontend/src/pages/welcome.vue
index 2e6bb8b38f..f7d262cc8a 100644
--- a/packages/frontend/src/pages/welcome.vue
+++ b/packages/frontend/src/pages/welcome.vue
@@ -11,22 +11,22 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { computed } from 'vue';
+import { computed, ref } from 'vue';
import XSetup from './welcome.setup.vue';
import XEntrance from './welcome.entrance.a.vue';
import { instanceName } from '@/config.js';
import * as os from '@/os.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';
-let meta = $ref(null);
+const meta = ref(null);
os.api('meta', { detail: true }).then(res => {
- meta = res;
+ meta.value = res;
});
-const headerActions = $computed(() => []);
+const headerActions = computed(() => []);
-const headerTabs = $computed(() => []);
+const headerTabs = computed(() => []);
definePageMetadata(computed(() => ({
title: instanceName,
diff --git a/packages/frontend/src/plugin.ts b/packages/frontend/src/plugin.ts
index e24f646a35..5e49af4858 100644
--- a/packages/frontend/src/plugin.ts
+++ b/packages/frontend/src/plugin.ts
@@ -96,7 +96,7 @@ function createPluginEnv(opts: { plugin: Plugin; storageKey: string }): Record<s
}),
'Plugin:open_url': values.FN_NATIVE(([url]) => {
utils.assertString(url);
- window.open(url.value, '_blank');
+ window.open(url.value, '_blank', 'noopener');
}),
'Plugin:config': values.OBJ(config),
};
diff --git a/packages/frontend/src/router.ts b/packages/frontend/src/router.ts
index b148325c71..b861afa9a3 100644
--- a/packages/frontend/src/router.ts
+++ b/packages/frontend/src/router.ts
@@ -55,6 +55,10 @@ export const routes = [{
name: 'profile',
component: page(() => import('./pages/settings/profile.vue')),
}, {
+ path: '/avatar-decoration',
+ name: 'avatarDecoration',
+ component: page(() => import('./pages/settings/avatar-decoration.vue')),
+ }, {
path: '/roles',
name: 'roles',
component: page(() => import('./pages/settings/roles.vue')),
@@ -63,9 +67,9 @@ export const routes = [{
name: 'privacy',
component: page(() => import('./pages/settings/privacy.vue')),
}, {
- path: '/reaction',
- name: 'reaction',
- component: page(() => import('./pages/settings/reaction.vue')),
+ path: '/emoji-picker',
+ name: 'emojiPicker',
+ component: page(() => import('./pages/settings/emoji-picker.vue')),
}, {
path: '/drive',
name: 'drive',
diff --git a/packages/frontend/src/scripts/aiscript/api.ts b/packages/frontend/src/scripts/aiscript/api.ts
index fb7ab924b7..038ae23109 100644
--- a/packages/frontend/src/scripts/aiscript/api.ts
+++ b/packages/frontend/src/scripts/aiscript/api.ts
@@ -50,6 +50,7 @@ export function createAiScriptEnv(opts) {
return values.ERROR('request_failed', utils.jsToVal(err));
});
}),
+ /* セキュリティ上の問題があるため無効化
'Mk:apiExternal': values.FN_NATIVE(async ([host, ep, param, token]) => {
utils.assertString(host);
utils.assertString(ep);
@@ -60,6 +61,7 @@ export function createAiScriptEnv(opts) {
return values.ERROR('request_failed', utils.jsToVal(err));
});
}),
+ */
'Mk:save': values.FN_NATIVE(([key, value]) => {
utils.assertString(key);
miLocalStorage.setItem(`aiscript:${opts.storageKey}:${key.value}`, JSON.stringify(utils.valToJs(value)));
diff --git a/packages/frontend/src/scripts/aiscript/ui.ts b/packages/frontend/src/scripts/aiscript/ui.ts
index d326b956e8..75b9248432 100644
--- a/packages/frontend/src/scripts/aiscript/ui.ts
+++ b/packages/frontend/src/scripts/aiscript/ui.ts
@@ -121,6 +121,7 @@ export type AsUiPostFormButton = AsUiComponentBase & {
rounded?: boolean;
form?: {
text: string;
+ cw?: string;
};
};
@@ -128,6 +129,7 @@ export type AsUiPostForm = AsUiComponentBase & {
type: 'postForm';
form?: {
text: string;
+ cw?: string;
};
};
@@ -454,8 +456,11 @@ function getPostFormButtonOptions(def: values.Value | undefined, call: (fn: valu
const getForm = () => {
const text = form!.value.get('text');
utils.assertString(text);
+ const cw = form!.value.get('cw');
+ if (cw) utils.assertString(cw);
return {
text: text.value,
+ cw: cw?.value,
};
};
@@ -478,8 +483,11 @@ function getPostFormOptions(def: values.Value | undefined, call: (fn: values.VFn
const getForm = () => {
const text = form!.value.get('text');
utils.assertString(text);
+ const cw = form!.value.get('cw');
+ if (cw) utils.assertString(cw);
return {
text: text.value,
+ cw: cw?.value,
};
};
diff --git a/packages/frontend/src/scripts/api.ts b/packages/frontend/src/scripts/api.ts
index 080977e5e4..8f3a163938 100644
--- a/packages/frontend/src/scripts/api.ts
+++ b/packages/frontend/src/scripts/api.ts
@@ -10,7 +10,12 @@ import { $i } from '@/account.js';
export const pendingApiRequestsCount = ref(0);
// Implements Misskey.api.ApiClient.request
-export function api<E extends keyof Misskey.Endpoints, P extends Misskey.Endpoints[E]['req']>(endpoint: E, data: P = {} as any, token?: string | null | undefined, signal?: AbortSignal): Promise<Misskey.Endpoints[E]['res']> {
+export function api<E extends keyof Misskey.Endpoints, P extends Misskey.Endpoints[E]['req']>(
+ endpoint: E,
+ data: P = {} as any,
+ token?: string | null | undefined,
+ signal?: AbortSignal,
+): Promise<Misskey.api.SwitchCaseResponseType<E, P>> {
if (endpoint.includes('://')) throw new Error('invalid endpoint');
pendingApiRequestsCount.value++;
@@ -51,51 +56,11 @@ export function api<E extends keyof Misskey.Endpoints, P extends Misskey.Endpoin
return promise;
}
-export function apiExternal<E extends keyof Misskey.Endpoints, P extends Misskey.Endpoints[E]['req']>(hostUrl: string, endpoint: E, data: P = {} as any, token?: string | null | undefined, signal?: AbortSignal): Promise<Misskey.Endpoints[E]['res']> {
- if (!/^https?:\/\//.test(hostUrl)) throw new Error('invalid host name');
- if (endpoint.includes('://')) throw new Error('invalid endpoint');
- pendingApiRequestsCount.value++;
-
- const onFinally = () => {
- pendingApiRequestsCount.value--;
- };
-
- const promise = new Promise<Misskey.Endpoints[E]['res'] | void>((resolve, reject) => {
- // Append a credential
- (data as any).i = token;
-
- const fullUrl = (hostUrl.slice(-1) === '/' ? hostUrl.slice(0, -1) : hostUrl)
- + '/api/' + (endpoint.slice(0, 1) === '/' ? endpoint.slice(1) : endpoint);
- // Send request
- window.fetch(fullUrl, {
- method: 'POST',
- body: JSON.stringify(data),
- credentials: 'omit',
- cache: 'no-cache',
- headers: {
- 'Content-Type': 'application/json',
- },
- signal,
- }).then(async (res) => {
- const body = res.status === 204 ? null : await res.json();
-
- if (res.status === 200) {
- resolve(body);
- } else if (res.status === 204) {
- resolve();
- } else {
- reject(body.error);
- }
- }).catch(reject);
- });
-
- promise.then(onFinally, onFinally);
-
- return promise;
-}
-
// Implements Misskey.api.ApiClient.request
-export function apiGet <E extends keyof Misskey.Endpoints, P extends Misskey.Endpoints[E]['req']>(endpoint: E, data: P = {} as any): Promise<Misskey.Endpoints[E]['res']> {
+export function apiGet<E extends keyof Misskey.Endpoints, P extends Misskey.Endpoints[E]['req']>(
+ endpoint: E,
+ data: P = {} as any,
+): Promise<Misskey.api.SwitchCaseResponseType<E, P>> {
pendingApiRequestsCount.value++;
const onFinally = () => {
diff --git a/packages/frontend/src/scripts/autocomplete.ts b/packages/frontend/src/scripts/autocomplete.ts
index 0d6756d498..6f3d3ba8e1 100644
--- a/packages/frontend/src/scripts/autocomplete.ts
+++ b/packages/frontend/src/scripts/autocomplete.ts
@@ -8,6 +8,8 @@ import getCaretCoordinates from 'textarea-caret';
import { toASCII } from 'punycode/';
import { popup } from '@/os.js';
+export type SuggestionType = 'user' | 'hashtag' | 'emoji' | 'mfmTag';
+
export class Autocomplete {
private suggestion: {
x: Ref<number>;
@@ -19,6 +21,7 @@ export class Autocomplete {
private currentType: string;
private textRef: Ref<string>;
private opening: boolean;
+ private onlyType: SuggestionType[];
private get text(): string {
// Use raw .value to get the latest value
@@ -35,7 +38,7 @@ export class Autocomplete {
/**
* 対象のテキストエリアを与えてインスタンスを初期化します。
*/
- constructor(textarea: HTMLInputElement | HTMLTextAreaElement, textRef: Ref<string>) {
+ constructor(textarea: HTMLInputElement | HTMLTextAreaElement, textRef: Ref<string>, onlyType?: SuggestionType[]) {
//#region BIND
this.onInput = this.onInput.bind(this);
this.complete = this.complete.bind(this);
@@ -46,6 +49,7 @@ export class Autocomplete {
this.textarea = textarea;
this.textRef = textRef;
this.opening = false;
+ this.onlyType = onlyType ?? ['user', 'hashtag', 'emoji', 'mfmTag'];
this.attach();
}
@@ -95,7 +99,7 @@ export class Autocomplete {
let opened = false;
- if (isMention) {
+ if (isMention && this.onlyType.includes('user')) {
const username = text.substring(mentionIndex + 1);
if (username !== '' && username.match(/^[a-zA-Z0-9_.]+$/)) {
this.open('user', username);
@@ -106,7 +110,7 @@ export class Autocomplete {
}
}
- if (isHashtag && !opened) {
+ if (isHashtag && !opened && this.onlyType.includes('hashtag')) {
const hashtag = text.substring(hashtagIndex + 1);
if (!hashtag.includes(' ')) {
this.open('hashtag', hashtag);
@@ -114,7 +118,7 @@ export class Autocomplete {
}
}
- if (isEmoji && !opened) {
+ if (isEmoji && !opened && this.onlyType.includes('emoji')) {
const emoji = text.substring(emojiIndex + 1);
if (!emoji.includes(' ')) {
this.open('emoji', emoji);
@@ -122,7 +126,7 @@ export class Autocomplete {
}
}
- if (isMfmTag && !opened) {
+ if (isMfmTag && !opened && this.onlyType.includes('mfmTag')) {
const mfmTag = text.substring(mfmTagIndex + 1);
if (!mfmTag.includes(' ')) {
this.open('mfmTag', mfmTag.replace('[', ''));
diff --git a/packages/frontend/src/scripts/clear-cache.ts b/packages/frontend/src/scripts/clear-cache.ts
new file mode 100644
index 0000000000..f2db87c4fb
--- /dev/null
+++ b/packages/frontend/src/scripts/clear-cache.ts
@@ -0,0 +1,15 @@
+import { unisonReload } from '@/scripts/unison-reload.js';
+import * as os from '@/os.js';
+import { miLocalStorage } from '@/local-storage.js';
+import { fetchCustomEmojis } from '@/custom-emojis.js';
+
+export async function clearCache() {
+ os.waiting();
+ miLocalStorage.removeItem('locale');
+ miLocalStorage.removeItem('localeVersion');
+ miLocalStorage.removeItem('theme');
+ miLocalStorage.removeItem('emojis');
+ miLocalStorage.removeItem('lastEmojisFetchedAt');
+ await fetchCustomEmojis(true);
+ unisonReload();
+}
diff --git a/packages/frontend/src/scripts/emoji-picker.ts b/packages/frontend/src/scripts/emoji-picker.ts
new file mode 100644
index 0000000000..f87c3f6fb2
--- /dev/null
+++ b/packages/frontend/src/scripts/emoji-picker.ts
@@ -0,0 +1,60 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and other misskey contributors
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { defineAsyncComponent, Ref, ref } from 'vue';
+import { popup } from '@/os.js';
+import { defaultStore } from '@/store.js';
+
+/**
+ * 絵文字ピッカーを表示する。
+ * 類似の機能として{@link ReactionPicker}が存在しているが、この機能とは動きが異なる。
+ * 投稿フォームなどで絵文字を選択する時など、絵文字ピックアップ後でもダイアログが消えずに残り、
+ * 一度表示したダイアログを連続で使用できることが望ましいシーンでの利用が想定される。
+ */
+class EmojiPicker {
+ private src: Ref<HTMLElement | null> = ref(null);
+ private manualShowing = ref(false);
+ private onChosen?: (emoji: string) => void;
+ private onClosed?: () => void;
+
+ constructor() {
+ // nop
+ }
+
+ public async init() {
+ const emojisRef = defaultStore.reactiveState.pinnedEmojis;
+ await popup(defineAsyncComponent(() => import('@/components/MkEmojiPickerDialog.vue')), {
+ src: this.src,
+ pinnedEmojis: emojisRef,
+ asReactionPicker: false,
+ manualShowing: this.manualShowing,
+ choseAndClose: false,
+ }, {
+ done: emoji => {
+ if (this.onChosen) this.onChosen(emoji);
+ },
+ close: () => {
+ this.manualShowing.value = false;
+ },
+ closed: () => {
+ this.src.value = null;
+ if (this.onClosed) this.onClosed();
+ },
+ });
+ }
+
+ public show(
+ src: HTMLElement,
+ onChosen?: EmojiPicker['onChosen'],
+ onClosed?: EmojiPicker['onClosed'],
+ ) {
+ this.src.value = src;
+ this.manualShowing.value = true;
+ this.onChosen = onChosen;
+ this.onClosed = onClosed;
+ }
+}
+
+export const emojiPicker = new EmojiPicker();
diff --git a/packages/frontend/src/scripts/emojilist.ts b/packages/frontend/src/scripts/emojilist.ts
index 4159da84c8..8885bf4b7f 100644
--- a/packages/frontend/src/scripts/emojilist.ts
+++ b/packages/frontend/src/scripts/emojilist.ts
@@ -43,3 +43,9 @@ export function getEmojiName(char: string): string | null {
return emojilist[idx].name;
}
}
+
+export interface CustomEmojiFolderTree {
+ value: string;
+ category: string;
+ children: CustomEmojiFolderTree[];
+}
diff --git a/packages/frontend/src/scripts/get-drive-file-menu.ts b/packages/frontend/src/scripts/get-drive-file-menu.ts
index 87f3886847..d6a5b00c0b 100644
--- a/packages/frontend/src/scripts/get-drive-file-menu.ts
+++ b/packages/frontend/src/scripts/get-drive-file-menu.ts
@@ -82,7 +82,7 @@ export function getDriveFileMenu(file: Misskey.entities.DriveFile, folder?: Miss
to: `/my/drive/file/${file.id}`,
text: i18n.ts._fileViewer.title,
icon: 'ph-file-text ph-bold ph-lg',
- }, null, {
+ }, { type: 'divider' }, {
text: i18n.ts.rename,
icon: 'ph-textbox ph-bold ph-lg',
action: () => rename(file),
@@ -101,7 +101,7 @@ export function getDriveFileMenu(file: Misskey.entities.DriveFile, folder?: Miss
aspectRatio: NaN,
uploadFolder: folder ? folder.id : folder,
}),
- }] : [], null, {
+ }] : [], { type: 'divider' }, {
text: i18n.ts.createNoteFromTheFile,
icon: 'ph-pencil ph-bold ph-lg',
action: () => os.post({
@@ -118,7 +118,7 @@ export function getDriveFileMenu(file: Misskey.entities.DriveFile, folder?: Miss
text: i18n.ts.download,
icon: 'ph-download ph-bold ph-lg',
download: file.name,
- }, null, {
+ }, { type: 'divider' }, {
text: i18n.ts.delete,
icon: 'ph-trash ph-bold ph-lg',
danger: true,
@@ -126,7 +126,7 @@ export function getDriveFileMenu(file: Misskey.entities.DriveFile, folder?: Miss
}];
if (defaultStore.state.devMode) {
- menu = menu.concat([null, {
+ menu = menu.concat([{ type: 'divider' }, {
icon: 'ph-identification-card ph-bold ph-lg',
text: i18n.ts.copyFileId,
action: () => {
diff --git a/packages/frontend/src/scripts/get-note-menu.ts b/packages/frontend/src/scripts/get-note-menu.ts
index e64c08c0ab..e23986ea4a 100644
--- a/packages/frontend/src/scripts/get-note-menu.ts
+++ b/packages/frontend/src/scripts/get-note-menu.ts
@@ -18,6 +18,7 @@ import { getUserMenu } from '@/scripts/get-user-menu.js';
import { clipsCache } from '@/cache.js';
import { MenuItem } from '@/types/menu.js';
import MkRippleEffect from '@/components/MkRippleEffect.vue';
+import { isSupportShare } from '@/scripts/navigator.js';
export async function getNoteClipMenu(props: {
note: Misskey.entities.Note;
@@ -60,7 +61,7 @@ export async function getNoteClipMenu(props: {
},
);
},
- })), null, {
+ })), { type: 'divider' }, {
icon: 'ph-plus ph-bold ph-lg',
text: i18n.ts.createNew,
action: async () => {
@@ -93,7 +94,7 @@ export async function getNoteClipMenu(props: {
}];
}
-export function getAbuseNoteMenu(note: misskey.entities.Note, text: string): MenuItem {
+export function getAbuseNoteMenu(note: Misskey.entities.Note, text: string): MenuItem {
return {
icon: 'ph-warning-circle ph-bold ph-lg',
text,
@@ -107,7 +108,7 @@ export function getAbuseNoteMenu(note: misskey.entities.Note, text: string): Men
};
}
-export function getCopyNoteLinkMenu(note: misskey.entities.Note, text: string): MenuItem {
+export function getCopyNoteLinkMenu(note: Misskey.entities.Note, text: string): MenuItem {
return {
icon: 'ph-link ph-bold ph-lg',
text,
@@ -285,7 +286,7 @@ export function getNoteMenu(props: {
text: i18n.ts.unclip,
danger: true,
action: unclip,
- }, null] : []
+ }, { type: 'divider' }] : []
), {
icon: 'ph-info ph-bold ph-lg',
text: i18n.ts.details,
@@ -302,20 +303,20 @@ export function getNoteMenu(props: {
icon: 'ph-arrow-square-out ph-bold ph-lg',
text: i18n.ts.showOnRemote,
action: () => {
- window.open(appearNote.url ?? appearNote.uri, '_blank');
+ window.open(appearNote.url ?? appearNote.uri, '_blank', 'noopener');
},
} : undefined,
- {
+ ...(isSupportShare() ? [{
icon: 'ph-share-network ph-bold ph-lg',
text: i18n.ts.share,
action: share,
- },
+ }] : []),
$i && $i.policies.canUseTranslator && instance.translatorAvailable ? {
icon: 'ph-translate ph-bold ph-lg',
text: i18n.ts.translate,
action: translate,
} : undefined,
- null,
+ { type: 'divider' },
statePromise.then(state => state.isFavorited ? {
icon: 'ph-star-half ph-bold ph-lg',
text: i18n.ts.unfavorite,
@@ -362,7 +363,7 @@ export function getNoteMenu(props: {
},
/*
...($i.isModerator || $i.isAdmin ? [
- null,
+ { type: 'divider' },
{
icon: 'ph-megaphone ph-bold ph-lg',
text: i18n.ts.promote,
@@ -371,13 +372,13 @@ export function getNoteMenu(props: {
: []
),*/
...(appearNote.userId !== $i.id ? [
- null,
+ { type: 'divider' },
appearNote.userId !== $i.id ? getAbuseNoteMenu(appearNote, i18n.ts.reportAbuse) : undefined,
]
: []
),
...(appearNote.userId === $i.id || $i.isModerator || $i.isAdmin ? [
- null,
+ { type: 'divider' },
appearNote.userId === $i.id ? {
icon: 'ph-pencil ph-bold ph-lg',
text: i18n.ts.edit,
@@ -415,14 +416,14 @@ export function getNoteMenu(props: {
icon: 'ph-arrow-square-out ph-bold ph-lg',
text: i18n.ts.showOnRemote,
action: () => {
- window.open(appearNote.url ?? appearNote.uri, '_blank');
+ window.open(appearNote.url ?? appearNote.uri, '_blank', 'noopener');
},
} : undefined]
.filter(x => x !== undefined);
}
if (noteActions.length > 0) {
- menu = menu.concat([null, ...noteActions.map(action => ({
+ menu = menu.concat([{ type: "divider" }, ...noteActions.map(action => ({
icon: 'ph-plug ph-bold ph-lg',
text: action.title,
action: () => {
@@ -432,7 +433,7 @@ export function getNoteMenu(props: {
}
if (defaultStore.state.devMode) {
- menu = menu.concat([null, {
+ menu = menu.concat([{ type: "divider" }, {
icon: 'ph-identification-card ph-bold ph-lg',
text: i18n.ts.copyNoteId,
action: () => {
@@ -518,7 +519,7 @@ export function getRenoteMenu(props: {
}]);
}
- if (!appearNote.channel || appearNote.channel?.allowRenoteToExternal) {
+ if (!appearNote.channel || appearNote.channel.allowRenoteToExternal) {
normalRenoteItems.push(...[{
text: i18n.ts.renote,
icon: 'ti ti-repeat',
@@ -561,10 +562,9 @@ export function getRenoteMenu(props: {
}]);
}
- // nullを挟むことで区切り線を出せる
const renoteItems = [
...normalRenoteItems,
- ...(channelRenoteItems.length > 0 && normalRenoteItems.length > 0) ? [null] : [],
+ ...(channelRenoteItems.length > 0 && normalRenoteItems.length > 0) ? [{ type: 'divider' }] : [],
...channelRenoteItems,
];
diff --git a/packages/frontend/src/scripts/get-user-menu.ts b/packages/frontend/src/scripts/get-user-menu.ts
index 41d0df1b72..67bc781aef 100644
--- a/packages/frontend/src/scripts/get-user-menu.ts
+++ b/packages/frontend/src/scripts/get-user-menu.ts
@@ -119,7 +119,7 @@ export function getUserMenu(user: Misskey.entities.UserDetailed, router: Router
userId: user.id,
});
}
-
+
async function invalidateFollow() {
if (!await getConfirmed(i18n.ts.breakFollowConfirm)) return;
@@ -189,7 +189,7 @@ export function getUserMenu(user: Misskey.entities.UserDetailed, router: Router
const canonical = user.host === null ? `@${user.username}` : `@${user.username}@${user.host}`;
os.post({ specified: user, initialText: `${canonical} ` });
},
- }, null, {
+ }, { type: 'divider' }, {
icon: 'ph-pencil ph-bold ph-lg',
text: i18n.ts.editMemo,
action: () => {
@@ -313,7 +313,7 @@ export function getUserMenu(user: Misskey.entities.UserDetailed, router: Router
}]);
//}
- menu = menu.concat([null, {
+ menu = menu.concat([{ type: 'divider' }, {
icon: user.isMuted ? 'ph-eye ph-bold ph-lg' : 'ph-eye-slash ph-bold ph-lg',
text: user.isMuted ? i18n.ts.unmute : i18n.ts.mute,
action: toggleMute,
@@ -335,7 +335,7 @@ export function getUserMenu(user: Misskey.entities.UserDetailed, router: Router
}]);
}
- menu = menu.concat([null, {
+ menu = menu.concat([{ type: 'divider' }, {
icon: 'ph-warning-circle ph-bold ph-lg',
text: i18n.ts.reportAbuse,
action: reportAbuse,
@@ -343,15 +343,15 @@ export function getUserMenu(user: Misskey.entities.UserDetailed, router: Router
}
if (user.host !== null) {
- menu = menu.concat([null, {
+ menu = menu.concat([{ type: 'divider' }, {
icon: 'ph-arrows-counter-clockwise ph-bold ph-lg',
text: i18n.ts.updateRemoteUser,
action: userInfoUpdate,
}]);
}
-
+
if (defaultStore.state.devMode) {
- menu = menu.concat([null, {
+ menu = menu.concat([{ type: 'divider' }, {
icon: 'ph-identification-card ph-bold ph-lg',
text: i18n.ts.copyUserId,
action: () => {
@@ -361,7 +361,7 @@ export function getUserMenu(user: Misskey.entities.UserDetailed, router: Router
}
if ($i && meId === user.id) {
- menu = menu.concat([null, {
+ menu = menu.concat([{ type: 'divider' }, {
icon: 'ph-pencil ph-bold ph-lg',
text: i18n.ts.editProfile,
action: () => {
@@ -371,7 +371,7 @@ export function getUserMenu(user: Misskey.entities.UserDetailed, router: Router
}
if (userActions.length > 0) {
- menu = menu.concat([null, ...userActions.map(action => ({
+ menu = menu.concat([{ type: 'divider' }, ...userActions.map(action => ({
icon: 'ph-plug ph-bold ph-lg',
text: action.title,
action: () => {
diff --git a/packages/frontend/src/scripts/isFfVisibleForMe.ts b/packages/frontend/src/scripts/isFfVisibleForMe.ts
index 0567f3b34a..dc0e90d20a 100644
--- a/packages/frontend/src/scripts/isFfVisibleForMe.ts
+++ b/packages/frontend/src/scripts/isFfVisibleForMe.ts
@@ -6,11 +6,19 @@
import * as Misskey from 'misskey-js';
import { $i } from '@/account.js';
-export function isFfVisibleForMe(user: Misskey.entities.UserDetailed): boolean {
+export function isFollowingVisibleForMe(user: Misskey.entities.UserDetailed): boolean {
if ($i && $i.id === user.id) return true;
- if (user.ffVisibility === 'private') return false;
- if (user.ffVisibility === 'followers' && !user.isFollowing) return false;
+ if (user.followingVisibility === 'private') return false;
+ if (user.followingVisibility === 'followers' && !user.isFollowing) return false;
+
+ return true;
+}
+export function isFollowersVisibleForMe(user: Misskey.entities.UserDetailed): boolean {
+ if ($i && $i.id === user.id) return true;
+
+ if (user.followersVisibility === 'private') return false;
+ if (user.followersVisibility === 'followers' && !user.isFollowing) return false;
return true;
}
diff --git a/packages/frontend/src/scripts/media-has-audio.ts b/packages/frontend/src/scripts/media-has-audio.ts
new file mode 100644
index 0000000000..3421a38a76
--- /dev/null
+++ b/packages/frontend/src/scripts/media-has-audio.ts
@@ -0,0 +1,9 @@
+export default async function hasAudio(media: HTMLMediaElement) {
+ const cloned = media.cloneNode() as HTMLMediaElement;
+ cloned.muted = (cloned as typeof cloned & Partial<HTMLVideoElement>).playsInline = true;
+ cloned.play();
+ await new Promise((resolve) => cloned.addEventListener('playing', resolve));
+ const result = !!(cloned as any).audioTracks?.length || (cloned as any).mozHasAudio || !!(cloned as any).webkitAudioDecodedByteCount;
+ cloned.remove();
+ return result;
+}
diff --git a/packages/frontend/src/scripts/navigator.ts b/packages/frontend/src/scripts/navigator.ts
new file mode 100644
index 0000000000..b13186a10e
--- /dev/null
+++ b/packages/frontend/src/scripts/navigator.ts
@@ -0,0 +1,8 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and other misskey contributors
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+export function isSupportShare(): boolean {
+ return 'share' in navigator;
+}
diff --git a/packages/frontend/src/scripts/page-metadata.ts b/packages/frontend/src/scripts/page-metadata.ts
index 330ba8da83..369e46aae1 100644
--- a/packages/frontend/src/scripts/page-metadata.ts
+++ b/packages/frontend/src/scripts/page-metadata.ts
@@ -15,6 +15,7 @@ export type PageMetadata = {
icon?: string | null;
avatar?: Misskey.entities.User | null;
userName?: Misskey.entities.User | null;
+ needWideArea?: boolean;
};
export function definePageMetadata(metadata: PageMetadata | null | Ref<PageMetadata | null> | ComputedRef<PageMetadata | null>): void {
diff --git a/packages/frontend/src/scripts/post-message.ts b/packages/frontend/src/scripts/post-message.ts
new file mode 100644
index 0000000000..80441caf15
--- /dev/null
+++ b/packages/frontend/src/scripts/post-message.ts
@@ -0,0 +1,25 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and other misskey contributors
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+export const postMessageEventTypes = [
+ 'misskey:shareForm:shareCompleted',
+] as const;
+
+export type PostMessageEventType = typeof postMessageEventTypes[number];
+
+export type MiPostMessageEvent = {
+ type: PostMessageEventType;
+ payload?: any;
+};
+
+/**
+ * 親フレームにイベントを送信
+ */
+export function postMessageToParentWindow(type: PostMessageEventType, payload?: any): void {
+ window.postMessage({
+ type,
+ payload,
+ }, '*');
+}
diff --git a/packages/frontend/src/scripts/reaction-picker.ts b/packages/frontend/src/scripts/reaction-picker.ts
index 19e1bfba2c..9b13e794f5 100644
--- a/packages/frontend/src/scripts/reaction-picker.ts
+++ b/packages/frontend/src/scripts/reaction-picker.ts
@@ -5,6 +5,7 @@
import { defineAsyncComponent, Ref, ref } from 'vue';
import { popup } from '@/os.js';
+import { defaultStore } from '@/store.js';
class ReactionPicker {
private src: Ref<HTMLElement | null> = ref(null);
@@ -17,25 +18,27 @@ class ReactionPicker {
}
public async init() {
+ const reactionsRef = defaultStore.reactiveState.reactions;
await popup(defineAsyncComponent(() => import('@/components/MkEmojiPickerDialog.vue')), {
src: this.src,
+ pinnedEmojis: reactionsRef,
asReactionPicker: true,
manualShowing: this.manualShowing,
}, {
done: reaction => {
- this.onChosen!(reaction);
+ if (this.onChosen) this.onChosen(reaction);
},
close: () => {
this.manualShowing.value = false;
},
closed: () => {
this.src.value = null;
- this.onClosed!();
+ if (this.onClosed) this.onClosed();
},
});
}
- public show(src: HTMLElement, onChosen: ReactionPicker['onChosen'], onClosed: ReactionPicker['onClosed']) {
+ public show(src: HTMLElement, onChosen?: ReactionPicker['onChosen'], onClosed?: ReactionPicker['onClosed']) {
this.src.value = src;
this.manualShowing.value = true;
this.onChosen = onChosen;
diff --git a/packages/frontend/src/scripts/snowfall-effect.ts b/packages/frontend/src/scripts/snowfall-effect.ts
new file mode 100644
index 0000000000..a09f02cec0
--- /dev/null
+++ b/packages/frontend/src/scripts/snowfall-effect.ts
@@ -0,0 +1,476 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and other misskey contributors
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+export class SnowfallEffect {
+ private VERTEX_SOURCE = `#version 300 es
+ in vec4 a_position;
+ in vec4 a_color;
+ in vec3 a_rotation;
+ in vec3 a_speed;
+ in float a_size;
+ out vec4 v_color;
+ out float v_rotation;
+ uniform float u_time;
+ uniform mat4 u_projection;
+ uniform vec3 u_worldSize;
+ uniform float u_gravity;
+ uniform float u_wind;
+
+ void main() {
+ v_color = a_color;
+ v_rotation = a_rotation.x + u_time * a_rotation.y;
+
+ vec3 pos = a_position.xyz;
+
+ float turbulence = 1.0;
+
+ pos.x = mod(pos.x + u_time + u_wind * a_speed.x, u_worldSize.x * 2.0) - u_worldSize.x;
+ pos.y = mod(pos.y - u_time * a_speed.y * u_gravity, u_worldSize.y * 2.0) - u_worldSize.y;
+
+ pos.x += sin(u_time * a_speed.z * turbulence) * a_rotation.z;
+ pos.z += cos(u_time * a_speed.z * turbulence) * a_rotation.z;
+
+ gl_Position = u_projection * vec4(pos.xyz, a_position.w);
+ gl_PointSize = (a_size / gl_Position.w) * 100.0;
+ }
+ `;
+
+ private FRAGMENT_SOURCE = `#version 300 es
+ precision highp float;
+
+ in vec4 v_color;
+ in float v_rotation;
+ uniform sampler2D u_texture;
+ out vec4 out_color;
+
+ void main() {
+ vec2 rotated = vec2(
+ cos(v_rotation) * (gl_PointCoord.x - 0.5) + sin(v_rotation) * (gl_PointCoord.y - 0.5) + 0.5,
+ cos(v_rotation) * (gl_PointCoord.y - 0.5) - sin(v_rotation) * (gl_PointCoord.x - 0.5) + 0.5
+ );
+
+ vec4 snowflake = texture(u_texture, rotated);
+
+ out_color = vec4(snowflake.rgb * v_color.xyz, snowflake.a * v_color.a);
+ }
+ `;
+
+ private gl: WebGLRenderingContext;
+ private program: WebGLProgram;
+ private canvas: HTMLCanvasElement;
+ private buffers: Record<string, {
+ size: number;
+ value: number[] | Float32Array;
+ location: number;
+ ref: WebGLBuffer;
+ }>;
+ private uniforms: Record<string, {
+ type: string;
+ value: number[] | Float32Array;
+ location: WebGLUniformLocation;
+ }>;
+ private texture: WebGLTexture;
+ private camera: {
+ fov: number;
+ near: number;
+ far: number;
+ aspect: number;
+ z: number;
+ };
+ private wind: {
+ current: number;
+ force: number;
+ target: number;
+ min: number;
+ max: number;
+ easing: number;
+ };
+ private time: {
+ start: number;
+ previous: number;
+ } = {
+ start: 0,
+ previous: 0,
+ };
+ private raf = 0;
+
+ private density: number = 1 / 90;
+ private depth = 100;
+ private count = 1000;
+ private gravity = 100;
+ private speed: number = 1 / 10000;
+ private color: number[] = [1, 1, 1];
+ private opacity = 1;
+ private size = 4;
+ private snowflake = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAAAXNSR0IArs4c6QAAAAlwSFlzAAALEwAACxMBAJqcGAAAAVlpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IlhNUCBDb3JlIDUuNC4wIj4KICAgPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4KICAgICAgPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIKICAgICAgICAgICAgeG1sbnM6dGlmZj0iaHR0cDovL25zLmFkb2JlLmNvbS90aWZmLzEuMC8iPgogICAgICAgICA8dGlmZjpPcmllbnRhdGlvbj4xPC90aWZmOk9yaWVudGF0aW9uPgogICAgICA8L3JkZjpEZXNjcmlwdGlvbj4KICAgPC9yZGY6UkRGPgo8L3g6eG1wbWV0YT4KTMInWQAAErRJREFUeAHdmgnYlmPax5MShaxRKRElPmXJXpaSsRxDU0bTZ+kt65RloiRDltEMQsxYKmS+zzYjxCCamCzV2LchResMIxFRQ1G93+93Pdf5dL9v7zuf4/hm0fc/jt9znddy3/e1nNd53c/7vHXq/AtVWVnZA/bzkaQjoWG298DeMdvrmP6/EIOqC4fBsbAx7Arz4TaYBPXgWVDnO2jSBrB2T0IMIA9mCmmoE8aonPkR6WPZHlp9xSlfeyeBzq9bHBD5feEdUGfDXBgBqnde+a2wvw/dYdNctvZNAp1PnTaFttA6JgP7eVgBM0CNzgO9HNvy0AcYDda6SaDTdXOnz8X+IkZDugAGQmOYA+ob6Ah/MIOMDRPhJjgJ6uV7pXtWt81/50SnY/Wvwn4ZDHAvwJ9ATYcxyaqsnEnqZCyCPaE80BgYZXG/5A3VyyP/b08LHa11z9KmFUwA5eqruRBHYX1s8WSI1Xcbme8Mt8PWUCU+kF8XbFN+dtH+p06OD4IU8EjD/VOZ5bnezq0XHcHuC2oV7BDlkVIWq56uIX8UjAO31GRIMYW0Vo/xXtSXJyTuXVO6xk1qalRTmQ9AfqzEvog2XYpllnsd6Qr4unCPT7NtByu0uU7vuAaOoy1JuvfXpJdTvSX0gI1gCXwGZdFmEFxoQb7Wid8s7lNu+I8wuHGsTqz2zpQ9DAa5R6HC55A2gvCMXthvwi25bjx26H0M9/9f4Rnok9s0zulFlC2HzzP9cnld8nH/p7DVrbmuIfYs6JLz9U3/z+KGadDeCDsmwre7GyEifn/su8HVSsL2HeBn8CK8AW+B7u9R5yrPgyOjvSn5DWAaXAG2UU7CE9Ayt4k4sR1lX4LaLdd9gn2ftsL+Vtuh1Dp/elH1C8lvCdUj8kDK3gbP8XdhCnSC86rcsNSR9pQvhc/gVlB9bUfqoFNAy/mLrUROrpMwCtpBxBbTtLqkF4K6IF9rf57I9pnYekx5AS0P1VhopXso9pR5buC7+kewU86nFcB+BT4EXdIvNO73sRBubGTXLZtTtgp+DEb++bACdqBuJOlAaMMzLVM3whegNznQDtCb+pW5b8YY76euB5+7pxm0IbzCfS8m3Zf2q4T8/+4JNArXGoptpxz8LqDmQJq0Qnostt/sfIn5GygD4/Zeq7B7wljQO2yjB/QGj0Pjxz4wGdqXrkjXtCT/ISyDa6EPpHrSraFjvnecFpMoMx40Br3xSlD262rYObevddHTs2kYwWUG9uP5It/f1eU5Xw9btwoXPALbwYXcg+unG/KB3Rq8n9ddAOpn4Kr8BAaBcltcDo9D7Ouavig1o34x7F94xqPk74eLQH0MH8HvwS3SLPe9iheEG6f70KiuLpZv6sxG/Va5bFJOabaO7ucAvGEbeAH+AN1hV7iDOidQFz4A2oJb6D1YDhXZHkTqpL8EbqHDYRtwW20AsdIb8syl5N2e6dTAPB2mWYa+hE4Qk7I59iMwFZ70GlJlfyuTVfygs7Hyw7HbwI0w3Tak14BqEtdg7wVdIx8pZbtBUbrjZeA3vUPBANkU+sEehev8O4Db6QpwYm+D8II0KPKHwUFeQ3oLDIMN4WgID1yOPQ+MAXMhNAtju3ztmtuAypiAw7EXwo/Am+0NfUG5mknYc6GfGVIjsoFNuyuoh8COuDcd2LmwA9jWE8bB3Q7N4XrwWAz5XOXR+Tx4n6FgdHeB6sF/w2QwhlSXdXvl/jixx4NH8GW5LDzb7GrR4ES4F5QddB99CieAwStOAPegdUZ2B71F3AXbQSn3vJ1bYaYWrayh3NUPTcbYFExVW3CfXwlvgfoavMbnDAY9dxGo6dCt0LeaB54H4UydDEPA2R4PDlrFLB9XuNmTlO+Xr7X9ZNBr9J4+EN8AMcv6ButpMND9FM6EnTOHkLrSnvtzwbbq3vwMB2ow/qWFSC8ZC++ZQaldbquH2afQWbl8TdcvVtC6LtipifAuOKt6gA9Tzqgzb5R2gP1hX3DVtZVHVvdklY5DA5beIkVPuZn8LOgAnWEfeAaUkxCan/voBNkfF+U5cFu5z5XlxZU20OmZtgm1K45VO4naNCukrcBZVk/CD+E/YBjoYjXJY8Zg9DxsDrbbBHTRotxOrug4eBs+hHgWZtKzfHrdXHBi9gDvqzxFHNA5KVfyBCf0ExgB7nkXStLLEKkniNf0AzUs5+ublkVFKiC9FBZAvGxshT0NnN3zoSUYSJQPcjAvm0HmjcIPemNS96F6E36drFLwugx7EEzNZV/l9IjoEPkW4B7eFtYH9QKcBcfA/aCWgpPQOT+zMbb9fS3nDbYR2MdgV0S5aVlUhLs0w45IHi7sqnnGJ2E7CXqHWgZXgJ1y8KqpDUmfSLmSV5yB/XrpDqVP8ofmehNdOv7I0ShfP4yyJdl2a4SchI1gCXgkHgljYfvc1i3cs/SU1A9jQRpfri/b0Sal1RrtSj4ULyHprY5C6+6E1+EBULq0E+DK7A96iwqX0z4td8B3dCdob5gD3UB3j9fUcNuDKFOvgc+bZAZFf4Zgu/q/AGPMgfm+5ShPWay+k6I31BwAvVDRYL2cuqfUVTkfnTqvVFx5ai7/MXn3tp1UrtRkDWRsaAMjzaD08uJ1irz7+8ps/6ZYj90V3FKrQBkvmubULbN7vs7tZRyJV9w0ePLbQ4PcJspqXnkbhbgoGk/AVptZRxpB0hU7Mpc1x34cdgKPm1dzeTts9XPwlFAO5Au4BDbO7ZycO7J9A/Zh2b4A2+ucALefWpTrflDKVq4kHQBOoi9PO1qvsDeGd6AxXAJbQ5VxlFrW8EnDcJlTsOPcjElxL7WNy7AduC4f2+A/rSN/Hyg7YMBTxgqPUT3F2HAqtIb58GvQW86GqyG+ff4UWz0FBuH4UhaTal1vmAGfg98dfP4d4HPGwmwYAg+D2/J7uU0ap/YaolHZVbBj5d1DaSK8ADsmqiH2JIhgNRhbPZrbhSdZ5heVJGw7477VfYuaagMK2sM8iMloga1HXAt/AeWELgQnR/0Z7k3W6pe3xTn/JamTFPGnPMZSj6p90rA8YOziwHcnH/EgTovJlJ0LPSHkyrTKmZNJ+8KrYKBsCQeB0pWdBFNleieMgzjL44jejTK1CPSY0CiMdyOT09g6ni5O3Ceg51U4VNLaPSA3SDNEwwiKFdgHgANNrpjb7UVejYTYCuZ92DR42HYh8gfDJfAMqBi4dqxk+RrKGkD0YXNsA6AT5qCUXhBe5CR0gPCC4dhqKFwI1m1qX0hr94CotDE4aAd3PCyBX4Jyn+sNL5tBDsRAp3S7b5KVYwa2A0nHaO5AXBeDtnlMxizsW+HomLh8zX9R5sTeBSEn/cqc2Tvak9eDXCyP2PgbYWzn2gefHxT7+0Qu/h18DO7XmPWYcYqSXuHz2myb6G7RNs7meLgeMxXugbiPA3clQx0xtgNPGN819L7+oCzvm6zSx+EkI+Du3Pe0LbOd/jqc7dhG9Wib+mJ5jaJBuL8e4B5aAMpAomKlb8d+KZWUVnw+dgzKSdDtvKaLDyJ1ReZB7O0J2EV5Xwd8OsTJExNpu7Q1SJ8zgy7K93UCX4P4mr4udoyhPGDKygOP+tomIFarMw2d+cfgF2DnDVAGoBvzw33YTHgPDoXQ7Fx/Wy6YkdMrcrmrehO4Pz3WvP90cIVPgonwITg4973yu0XTZK0+ZQaQd+K816twVAwKO71ZRj9zeg7lcVzXHghpVN4n2G3BAHQ1NILx4MBjoppgLwL3Ww8IHZsf6vGk3O8fwx9heK7rhD0o2zdg75JtT6GzQQ8KzcZwElSr3M5J85ktYCzEG+Gx2NNzm/Cm5pSp+K2gfLrZbg3RcB2IQcZN1qPM3+l06SjbAltX/TiXe1wtg7+AdR+AcgIs7xUPw94XxuTrnOD4E1bEoe9Rptw+DWGOGeQi7JOs1SfKKfk+epcakPNxbI8uFVdem8vT6aJdq7jASYjOFPdQDP4Q6t+Em8HVutmbkbYH9Tv4LcQW+H6ujy9Wrtxc6A7vQnznb5TbHUPZ0mw7CeoaOBAegmfBIKw8WZzs34M/oNiPGPzB2KHdrVMUlD29VFLLpw2jMWmnaIbdDNxXur+dWgVumTMglI4zMgbUEV5LmjqW7XnRkDS9qhbu/xZlZ8LWuc3UfM22Of80aVcYDJ/lstdIWxXu0TGXm/TO19vveHWuOglUxOo6iMfyBe7JOEp01ech9puuuBCMA8pVcUUNUB5lqgMYwJyE1oXOGTh9v1gO6kmogKEwHtREMHYofz5zAl3lJ2AWqJfgfohJiKB8HWWfg54YA9Zr1fn5Xmm80SdvHhNwVmq2umF8vWxA+WRwwE9BPNhOulrq0nxz97j6Go6DF8HYcBfYyer6MwWuoINeDG6roq4iE97QCtsJuxWc2JrkCeKEbgX7waOgnLiavxdQEWfohtgRwCrygIoxoQv1K0FNgR7gAKPTB+dr5lAWMliqmbAb7AzbgCs42vYK21NmOiwHJ9atpdxqDlhdA75QdYJT4XUYDfbBiVRe5ySoZTAbBpeekp6T4lo5uFnBz0fpJ6P8E9SJufEdXHipdRA/mw2hzmvfhrfgfjCKPwJnwn2g3igldb4hNaD5a6/fz7eHVuAb2wPwPs+4DB7E/hTagd64BbgoC6Ab9IAfgn+OX0p/ppAaGxZjnw6+Ep8DK8Cj0IDrmHw3GaeN9EZ/AlxFfk1RuVGUYu8K00D9Fa6EvrAUVKzO29gXg9vC1VW3g540w0xBcU2hKJnz+FxYvTCXWaduK/StuTZlLcD6JjnfEvsb6A56m32z78q4FMGw1gA4lEa60WmwMeiSnsljIBSDmEOBE3RdfvggbMuMIbNhItgJtbyUpE9ddjA0Bid1sderXDaQ1OdPAO9zH6hDcpuG2Ml7SQfArHRx6Xpf3JTluySrsrIP6Seg9/iMqsEvF6YZoXIDeAZCRmpneAHEnnLQnaEuXATX53schR3n/e7YyuvOT1bpnyV107Io3xZ6QWs4EirAyXkEqqvK3xa9CQ0c5C5xQ+zN8kWjcr2xZxTsBHfmsipbP671ZmW3wHYA58DdEPobhtwVF2HfBE9H3pT8xjkdja3iiDK4PQBO8Dx4B9wiH8JKeANcKTUW9IITwKNMeYrcArfDhVDsb1pVyty26le5D97/zWzrzVUGXyVjI0WjHUgq4CjoAuGiRuuJkN7mSJX7cn+uaZNyfBBgDHZqXvqsU2cZ6aPwChgE/ap8M9wLbSH+0DKOaw18z8N12GPAyf4BfADbwBmwCbxAHY9NvxQXx2GgVLZXPvurZDE0rqk5+NmAm8U2aIbdH9yDalgpSS80ltlB29fPqW9c8XLUHnsIuGquqt8gN7edwtazrOsAn4MysLryX8BD4Ap3y+0dZROIwPsl9h/hHjgit4lXdrdvHN8dc91wyk7JdvIS7VpF46Jb2ZGz4WJIRyBpBKQW3oR8lZuSvwQMhKtAfQUpYuf27cgbNx6EEeDAzgMHPwYMYi2gEcSfxC7B9qicDMoo/1vQI8p9IG88WAY/yeVpYrJdHpf5vytu4Ky7X46xIamrvjDb52OrG3K+HrZt4xq9wYEZPGPVfp7bhsdE2os2ylV6J1n5mbYPUX4S7AkGX+OAk2t6mm1Iw3PtQ+O4LuooK26RYvW3s7nBLZDiAGlbUHYiRV/S5AWk28DTEFqB4eo+B+n1M55Ivhu4kspj92uYCm6Px0Gv61lor0fcDQNBrQQnOr71lVeYsm894L/bkBuFe/u93eBngJtJMlwTDIDKyfDt6n3se8Dt8jHoNU0o70waq34obZ8lPx4coG+LbifrP6Pt0aQvwn65LFzcAHY8ZUtgAnwExp2WoMpeQLvaA12p7bf/pLPFmS3a/ajr750cfE43wX4YYmU9wi7IddHBCsrc69vm8uuwQydYVhQVvmsUn7s+ebfD0GhXrI+yf2jqA4oPKdo+iHxMwHbYRmgjta4cUTqCWXkg0UHatIR4SxxWKK9PeXhgKiZfxWOthzXuGff4p6b54bH3Y3W3pNxJcK8ebgdI44iys0G0N/8qKGOAGg9Ni50n3yjy2GkxSKtMRtT/21I7Fg/H9lRIX6qK5YX6zSjvDL4BGiBfBnUNmFdzwfKX4Ct40OtJv1sDj0Hlzrk6xbM3tob7uCf4amyk96VHvQg7gltGzQG9wpcwX6BCesfJ3/kJiMmgs+Gm4errUeZqF+Up4IoOzoWLcmqETyLve/2BsKkFpGUvK7VYCz6j06RbQx+ogHhN3Qdb3QF+a/wVKF94OhSHR77sWcXytcKm82usHGW9QE2B3skq/QB7APaqnJ9NuvaufnF1GIhxYH3LSAeA+hM0hMfgNzATdHvjgDHDv+qkP8gW77XW2gwmYsJe2F3zZDgxI7NteTo+/1WD/B9Au3Zjh2RyrgAAAABJRU5ErkJggg==';
+
+ private INITIAL_BUFFERS = () => ({
+ position: { size: 3, value: [] },
+ color: { size: 4, value: [] },
+ size: { size: 1, value: [] },
+ rotation: { size: 3, value: [] },
+ speed: { size: 3, value: [] },
+ });
+
+ private INITIAL_UNIFORMS = () => ({
+ time: { type: 'float', value: 0 },
+ worldSize: { type: 'vec3', value: [0, 0, 0] },
+ gravity: { type: 'float', value: this.gravity },
+ wind: { type: 'float', value: 0 },
+ projection: {
+ type: 'mat4',
+ value: [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1],
+ },
+ });
+
+ private UNIFORM_SETTERS = {
+ int: 'uniform1i',
+ float: 'uniform1f',
+ vec2: 'uniform2fv',
+ vec3: 'uniform3fv',
+ vec4: 'uniform4fv',
+ mat2: 'uniformMatrix2fv',
+ mat3: 'uniformMatrix3fv',
+ mat4: 'uniformMatrix4fv',
+ };
+
+ private CAMERA = {
+ fov: 60,
+ near: 5,
+ far: 10000,
+ aspect: 1,
+ z: 100,
+ };
+
+ private WIND = {
+ current: 0,
+ force: 0.01,
+ target: 0.01,
+ min: 0,
+ max: 0.125,
+ easing: 0.0005,
+ };
+
+ constructor() {
+ const canvas = this.initCanvas();
+ const gl = canvas.getContext('webgl2', { antialias: true });
+ if (gl == null) throw new Error('Failed to get WebGL context');
+
+ document.body.append(canvas);
+
+ this.canvas = canvas;
+ this.gl = gl;
+ this.program = this.initProgram();
+ this.buffers = this.initBuffers();
+ this.uniforms = this.initUniforms();
+ this.texture = this.initTexture();
+ this.camera = this.initCamera();
+ this.wind = this.initWind();
+
+ this.resize = this.resize.bind(this);
+ this.update = this.update.bind(this);
+
+ window.addEventListener('resize', () => this.resize());
+ }
+
+ private initCanvas(): HTMLCanvasElement {
+ const canvas = document.createElement('canvas');
+
+ Object.assign(canvas.style, {
+ position: 'fixed',
+ top: 0,
+ left: 0,
+ width: '100vw',
+ height: '100vh',
+ background: 'transparent',
+ 'pointer-events': 'none',
+ 'z-index': 2147483647,
+ });
+
+ return canvas;
+ }
+
+ private initCamera() {
+ return { ...this.CAMERA };
+ }
+
+ private initWind() {
+ return { ...this.WIND };
+ }
+
+ private initShader(type, source): WebGLShader {
+ const { gl } = this;
+ const shader = gl.createShader(type);
+ if (shader == null) throw new Error('Failed to create shader');
+
+ gl.shaderSource(shader, source);
+ gl.compileShader(shader);
+
+ return shader;
+ }
+
+ private initProgram(): WebGLProgram {
+ const { gl } = this;
+ const vertex = this.initShader(gl.VERTEX_SHADER, this.VERTEX_SOURCE);
+ const fragment = this.initShader(gl.FRAGMENT_SHADER, this.FRAGMENT_SOURCE);
+ const program = gl.createProgram();
+ if (program == null) throw new Error('Failed to create program');
+
+ gl.attachShader(program, vertex);
+ gl.attachShader(program, fragment);
+ gl.linkProgram(program);
+ gl.useProgram(program);
+
+ return program;
+ }
+
+ private initBuffers(): SnowfallEffect['buffers'] {
+ const { gl, program } = this;
+ const buffers = this.INITIAL_BUFFERS() as unknown as SnowfallEffect['buffers'];
+
+ for (const [name, buffer] of Object.entries(buffers)) {
+ buffer.location = gl.getAttribLocation(program, `a_${name}`);
+ buffer.ref = gl.createBuffer()!;
+
+ gl.bindBuffer(gl.ARRAY_BUFFER, buffer.ref);
+ gl.enableVertexAttribArray(buffer.location);
+ gl.vertexAttribPointer(
+ buffer.location,
+ buffer.size,
+ gl.FLOAT,
+ false,
+ 0,
+ 0,
+ );
+ }
+
+ return buffers;
+ }
+
+ private updateBuffers() {
+ const { buffers } = this;
+
+ for (const name of Object.keys(buffers)) {
+ this.setBuffer(name);
+ }
+ }
+
+ private setBuffer(name: string, value?) {
+ const { gl, buffers } = this;
+ const buffer = buffers[name];
+
+ buffer.value = new Float32Array(value ?? buffer.value);
+
+ gl.bindBuffer(gl.ARRAY_BUFFER, buffer.ref);
+ gl.bufferData(gl.ARRAY_BUFFER, buffer.value, gl.STATIC_DRAW);
+ }
+
+ private initUniforms(): SnowfallEffect['uniforms'] {
+ const { gl, program } = this;
+ const uniforms = this.INITIAL_UNIFORMS() as unknown as SnowfallEffect['uniforms'];
+
+ for (const [name, uniform] of Object.entries(uniforms)) {
+ uniform.location = gl.getUniformLocation(program, `u_${name}`)!;
+ }
+
+ return uniforms;
+ }
+
+ private updateUniforms() {
+ const { uniforms } = this;
+
+ for (const name of Object.keys(uniforms)) {
+ this.setUniform(name);
+ }
+ }
+
+ private setUniform(name: string, value?) {
+ const { gl, uniforms } = this;
+ const uniform = uniforms[name];
+ const setter = this.UNIFORM_SETTERS[uniform.type];
+ const isMatrix = /^mat[2-4]$/i.test(uniform.type);
+
+ uniform.value = value ?? uniform.value;
+
+ if (isMatrix) {
+ gl[setter](uniform.location, false, uniform.value);
+ } else {
+ gl[setter](uniform.location, uniform.value);
+ }
+ }
+
+ private initTexture() {
+ const { gl } = this;
+ const texture = gl.createTexture();
+ if (texture == null) throw new Error('Failed to create texture');
+ const image = new Image();
+
+ gl.bindTexture(gl.TEXTURE_2D, texture);
+ gl.texImage2D(
+ gl.TEXTURE_2D,
+ 0,
+ gl.RGBA,
+ 1,
+ 1,
+ 0,
+ gl.RGBA,
+ gl.UNSIGNED_BYTE,
+ new Uint8Array([0, 0, 0, 0]),
+ );
+
+ image.onload = () => {
+ gl.bindTexture(gl.TEXTURE_2D, texture);
+ gl.texImage2D(
+ gl.TEXTURE_2D,
+ 0,
+ gl.RGBA,
+ gl.RGBA,
+ gl.UNSIGNED_BYTE,
+ image,
+ );
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
+ };
+
+ image.src = this.snowflake;
+
+ return texture;
+ }
+
+ private initSnowflakes(vw: number, vh: number, dpi: number) {
+ const position: number[] = [];
+ const color: number[] = [];
+ const size: number[] = [];
+ const rotation: number[] = [];
+ const speed: number[] = [];
+
+ const height = 1 / this.density;
+ const width = (vw / vh) * height;
+ const depth = this.depth;
+ const count = this.count;
+ const length = (vw / vh) * count;
+
+ for (let i = 0; i < length; ++i) {
+ position.push(
+ -width + Math.random() * width * 2,
+ -height + Math.random() * height * 2,
+ Math.random() * depth * 2,
+ );
+
+ speed.push(1 + Math.random(), 1 + Math.random(), Math.random() * 10);
+
+ rotation.push(
+ Math.random() * 2 * Math.PI,
+ Math.random() * 20,
+ Math.random() * 10,
+ );
+
+ color.push(...this.color, 0.1 + Math.random() * this.opacity);
+ //size.push((this.size * Math.random() * this.size * vh * dpi) / 1000);
+ size.push((this.size * vh * dpi) / 1000);
+ }
+
+ this.setUniform('worldSize', [width, height, depth]);
+
+ this.setBuffer('position', position);
+ this.setBuffer('color', color);
+ this.setBuffer('rotation', rotation);
+ this.setBuffer('size', size);
+ this.setBuffer('speed', speed);
+ }
+
+ private setProjection(aspect: number) {
+ const { camera } = this;
+
+ camera.aspect = aspect;
+
+ const fovRad = (camera.fov * Math.PI) / 180;
+ const f = Math.tan(Math.PI * 0.5 - 0.5 * fovRad);
+ const rangeInv = 1.0 / (camera.near - camera.far);
+
+ const m0 = f / camera.aspect;
+ const m5 = f;
+ const m10 = (camera.near + camera.far) * rangeInv;
+ const m11 = -1;
+ const m14 = camera.near * camera.far * rangeInv * 2 + camera.z;
+ const m15 = camera.z;
+
+ return [m0, 0, 0, 0, 0, m5, 0, 0, 0, 0, m10, m11, 0, 0, m14, m15];
+ }
+
+ public render() {
+ const { gl } = this;
+
+ gl.enable(gl.BLEND);
+ gl.enable(gl.CULL_FACE);
+ gl.blendFunc(gl.SRC_ALPHA, gl.ONE);
+ gl.disable(gl.DEPTH_TEST);
+
+ this.updateBuffers();
+ this.updateUniforms();
+ this.resize(true);
+
+ this.time = {
+ start: window.performance.now(),
+ previous: window.performance.now(),
+ };
+
+ if (this.raf) window.cancelAnimationFrame(this.raf);
+ this.raf = window.requestAnimationFrame(this.update);
+
+ return this;
+ }
+
+ private resize(updateSnowflakes = false) {
+ const { canvas, gl } = this;
+ const vw = canvas.offsetWidth;
+ const vh = canvas.offsetHeight;
+ const aspect = vw / vh;
+ const dpi = window.devicePixelRatio;
+
+ canvas.width = vw * dpi;
+ canvas.height = vh * dpi;
+
+ gl.viewport(0, 0, vw * dpi, vh * dpi);
+ gl.clearColor(0, 0, 0, 0);
+
+ if (updateSnowflakes === true) {
+ this.initSnowflakes(vw, vh, dpi);
+ }
+
+ this.setUniform('projection', this.setProjection(aspect));
+ }
+
+ private update(timestamp: number) {
+ const { gl, buffers, wind } = this;
+ const elapsed = (timestamp - this.time.start) * this.speed;
+ const delta = timestamp - this.time.previous;
+
+ gl.clear(gl.COLOR_BUFFER_BIT);
+ gl.drawArrays(
+ gl.POINTS,
+ 0,
+ buffers.position.value.length / buffers.position.size,
+ );
+
+ if (Math.random() > 0.995) {
+ wind.target =
+ (wind.min + Math.random() * (wind.max - wind.min)) *
+ (Math.random() > 0.5 ? -1 : 1);
+ }
+
+ wind.force += (wind.target - wind.force) * wind.easing;
+ wind.current += wind.force * (delta * 0.2);
+
+ this.setUniform('wind', wind.current);
+ this.setUniform('time', elapsed);
+
+ this.time.previous = timestamp;
+
+ this.raf = window.requestAnimationFrame(this.update);
+ }
+}
diff --git a/packages/frontend/src/scripts/sound.ts b/packages/frontend/src/scripts/sound.ts
index 4b0cd0bb39..2f7545ef0d 100644
--- a/packages/frontend/src/scripts/sound.ts
+++ b/packages/frontend/src/scripts/sound.ts
@@ -3,13 +3,22 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
+import type { SoundStore } from '@/store.js';
import { defaultStore } from '@/store.js';
+import * as os from '@/os.js';
-const ctx = new AudioContext();
+let ctx: AudioContext;
const cache = new Map<string, AudioBuffer>();
+let canPlay = true;
export const soundsTypes = [
+ // 音声なし
null,
+
+ // ドライブの音声
+ '_driveFile_',
+
+ // プリインストール
'syuilo/n-aec',
'syuilo/n-aec-4va',
'syuilo/n-aec-4vb',
@@ -38,6 +47,8 @@ export const soundsTypes = [
'syuilo/waon',
'syuilo/popo',
'syuilo/triple',
+ 'syuilo/bubble1',
+ 'syuilo/bubble2',
'syuilo/poi1',
'syuilo/poi2',
'syuilo/pirori',
@@ -61,46 +72,161 @@ export const soundsTypes = [
'noizenecio/kick_gaba7',
] as const;
-export async function getAudio(file: string, useCache = true) {
- if (useCache && cache.has(file)) {
- return cache.get(file)!;
+export const operationTypes = [
+ 'noteMy',
+ 'note',
+ 'antenna',
+ 'channel',
+ 'notification',
+ 'reaction',
+] as const;
+
+/** サウンドの種類 */
+export type SoundType = typeof soundsTypes[number];
+
+/** スプライトの種類 */
+export type OperationType = typeof operationTypes[number];
+
+/**
+ * 音声を読み込む
+ * @param soundStore サウンド設定
+ * @param options `useCache`: デフォルトは`true` 一度再生した音声はキャッシュする
+ */
+export async function loadAudio(soundStore: SoundStore, options?: { useCache?: boolean; }) {
+ if (_DEV_) console.log('loading audio. opts:', options);
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
+ if (soundStore.type === null || (soundStore.type === '_driveFile_' && !soundStore.fileUrl)) {
+ return;
+ }
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
+ if (ctx == null) {
+ ctx = new AudioContext();
+ }
+ if (options?.useCache ?? true) {
+ if (soundStore.type === '_driveFile_' && cache.has(soundStore.fileId)) {
+ if (_DEV_) console.log('use cache');
+ return cache.get(soundStore.fileId) as AudioBuffer;
+ } else if (cache.has(soundStore.type)) {
+ if (_DEV_) console.log('use cache');
+ return cache.get(soundStore.type) as AudioBuffer;
+ }
+ }
+
+ let response: Response;
+
+ if (soundStore.type === '_driveFile_') {
+ try {
+ response = await fetch(soundStore.fileUrl);
+ } catch (err) {
+ try {
+ // URLが変わっている可能性があるのでドライブ側からURLを取得するフォールバック
+ const apiRes = await os.api('drive/files/show', {
+ fileId: soundStore.fileId,
+ });
+ response = await fetch(apiRes.url);
+ } catch (fbErr) {
+ // それでも無理なら諦める
+ return;
+ }
+ }
+ } else {
+ try {
+ response = await fetch(`/client-assets/sounds/${soundStore.type}.mp3`);
+ } catch (err) {
+ return;
+ }
}
- const response = await fetch(`/client-assets/sounds/${file}.mp3`);
const arrayBuffer = await response.arrayBuffer();
const audioBuffer = await ctx.decodeAudioData(arrayBuffer);
- if (useCache) {
- cache.set(file, audioBuffer);
+ if (options?.useCache ?? true) {
+ if (soundStore.type === '_driveFile_') {
+ cache.set(soundStore.fileId, audioBuffer);
+ } else {
+ cache.set(soundStore.type, audioBuffer);
+ }
}
return audioBuffer;
}
-export function setVolume(audio: HTMLAudioElement, volume: number): HTMLAudioElement {
- const masterVolume = defaultStore.state.sound_masterVolume;
- audio.volume = masterVolume - ((1 - volume) * masterVolume);
- return audio;
+/**
+ * 既定のスプライトを再生する
+ * @param type スプライトの種類を指定
+ */
+export function play(operationType: OperationType) {
+ const sound = defaultStore.state[`sound_${operationType}`];
+ if (_DEV_) console.log('play', operationType, sound);
+ if (sound.type == null || !canPlay) return;
+
+ canPlay = false;
+ playFile(sound).finally(() => {
+ // ごく短時間に音が重複しないように
+ setTimeout(() => {
+ canPlay = true;
+ }, 25);
+ });
}
-export function play(type: 'noteMy' | 'note' | 'antenna' | 'channel' | 'notification') {
- const sound = defaultStore.state[`sound_${type}`];
- if (_DEV_) console.log('play', type, sound);
- if (sound.type == null) return;
- playFile(sound.type, sound.volume);
+/**
+ * サウンド設定形式で指定された音声を再生する
+ * @param soundStore サウンド設定
+ */
+export async function playFile(soundStore: SoundStore) {
+ const buffer = await loadAudio(soundStore);
+ if (!buffer) return;
+ createSourceNode(buffer, soundStore.volume)?.start();
}
-export async function playFile(file: string, volume: number) {
+export function createSourceNode(buffer: AudioBuffer, volume: number) : AudioBufferSourceNode | null {
const masterVolume = defaultStore.state.sound_masterVolume;
- if (masterVolume === 0 || volume === 0) {
- return;
+ if (isMute() || masterVolume === 0 || volume === 0) {
+ return null;
}
const gainNode = ctx.createGain();
gainNode.gain.value = masterVolume * volume;
const soundSource = ctx.createBufferSource();
- soundSource.buffer = await getAudio(file);
+ soundSource.buffer = buffer;
soundSource.connect(gainNode).connect(ctx.destination);
- soundSource.start();
+
+ return soundSource;
+}
+
+/**
+ * 音声の長さをミリ秒で取得する
+ * @param file ファイルのURL(ドライブIDではない)
+ */
+export async function getSoundDuration(file: string): Promise<number> {
+ const audioEl = document.createElement('audio');
+ audioEl.src = file;
+ return new Promise((resolve) => {
+ const si = setInterval(() => {
+ if (audioEl.readyState > 0) {
+ resolve(audioEl.duration * 1000);
+ clearInterval(si);
+ audioEl.remove();
+ }
+ }, 100);
+ });
+}
+
+/**
+ * ミュートすべきかどうかを判断する
+ */
+export function isMute(): boolean {
+ if (defaultStore.state.sound_notUseSound) {
+ // サウンドを出力しない
+ return true;
+ }
+
+ // noinspection RedundantIfStatementJS
+ if (defaultStore.state.sound_useSoundOnlyWhenActive && document.visibilityState === 'hidden') {
+ // ブラウザがアクティブな時のみサウンドを出力する
+ return true;
+ }
+
+ return false;
}
diff --git a/packages/frontend/src/scripts/theme.ts b/packages/frontend/src/scripts/theme.ts
index 22b8a5df37..3bf6d5798c 100644
--- a/packages/frontend/src/scripts/theme.ts
+++ b/packages/frontend/src/scripts/theme.ts
@@ -44,7 +44,7 @@ export const getBuiltinThemes = () => Promise.all(
'd-cherry',
'd-ice',
'd-u0',
- ].map(name => import(`../themes/${name}.json5`).then(({ default: _default }): Theme => _default)),
+ ].map(name => import(`@/themes/${name}.json5`).then(({ default: _default }): Theme => _default)),
);
export const getBuiltinThemesRef = () => {
diff --git a/packages/frontend/src/store.ts b/packages/frontend/src/store.ts
index 1f0aae3a3a..c86c3d01ad 100644
--- a/packages/frontend/src/store.ts
+++ b/packages/frontend/src/store.ts
@@ -6,6 +6,7 @@
import { markRaw, ref } from 'vue';
import * as Misskey from 'misskey-js';
import { miLocalStorage } from './local-storage.js';
+import type { SoundType } from '@/scripts/sound.js';
import { Storage } from '@/pizzax.js';
interface PostFormAction {
@@ -35,6 +36,22 @@ interface PageViewInterruptor {
handler: (page: Misskey.entities.Page) => unknown;
}
+/** サウンド設定 */
+export type SoundStore = {
+ type: Exclude<SoundType, '_driveFile_'>;
+ volume: number;
+} | {
+ type: '_driveFile_';
+
+ /** ドライブのファイルID */
+ fileId: string;
+
+ /** ファイルURL(こちらが優先される) */
+ fileUrl: string;
+
+ volume: number;
+}
+
export const postFormActions: PostFormAction[] = [];
export const userActions: UserAction[] = [];
export const noteActions: NoteAction[] = [];
@@ -114,6 +131,10 @@ export const defaultStore = markRaw(new Storage('base', {
where: 'account',
default: ['👍', '❤️', '😆', '🤔', '😮', '🎉', '💢', '😥', '😇', '🍮'],
},
+ pinnedEmojis: {
+ where: 'account',
+ default: [],
+ },
reactionAcceptance: {
where: 'account',
default: 'nonSensitiveOnly' as 'likeOnly' | 'likeOnlyForRemote' | 'nonSensitiveOnly' | 'nonSensitiveOnlyForLocalLikeOnlyForRemote' | null,
@@ -181,7 +202,7 @@ export const defaultStore = markRaw(new Storage('base', {
tl: {
where: 'deviceAccount',
default: {
- src: 'home' as 'home' | 'local' | 'social' | 'global' | `list:${string}`,
+ src: 'home' as 'home' | 'local' | 'social' | 'global' | 'bubble' | `list:${string}`,
userList: null as Misskey.entities.UserList | null,
},
},
@@ -226,10 +247,6 @@ export const defaultStore = markRaw(new Storage('base', {
where: 'device',
default: false,
},
- enableDataSaverMode: {
- where: 'device',
- default: false,
- },
disableShowingAnimatedImages: {
where: 'device',
default: window.matchMedia('(prefers-reduced-motion)').matches,
@@ -286,19 +303,19 @@ export const defaultStore = markRaw(new Storage('base', {
where: 'device',
default: 'remote' as 'none' | 'remote' | 'always',
},
- reactionPickerSize: {
+ emojiPickerScale: {
where: 'device',
default: 1,
},
- reactionPickerWidth: {
+ emojiPickerWidth: {
where: 'device',
default: 1,
},
- reactionPickerHeight: {
+ emojiPickerHeight: {
where: 'device',
default: 2,
},
- reactionPickerUseDrawerForMobile: {
+ emojiPickerUseDrawerForMobile: {
where: 'device',
default: true,
},
@@ -362,6 +379,10 @@ export const defaultStore = markRaw(new Storage('base', {
where: 'device',
default: 'medium' as 'small' | 'medium' | 'large',
},
+ limitWidthOfReaction: {
+ where: 'device',
+ default: true,
+ },
forceShowAds: {
where: 'device',
default: false,
@@ -422,30 +443,55 @@ export const defaultStore = markRaw(new Storage('base', {
where: 'device',
default: true,
},
+ dataSaver: {
+ where: 'device',
+ default: {
+ media: false,
+ avatar: false,
+ urlPreview: false,
+ code: false,
+ } as Record<string, boolean>,
+ },
+ enableSeasonalScreenEffect: {
+ where: 'device',
+ default: false,
+ },
sound_masterVolume: {
where: 'device',
default: 0.3,
},
+ sound_notUseSound: {
+ where: 'device',
+ default: false,
+ },
+ sound_useSoundOnlyWhenActive: {
+ where: 'device',
+ default: false,
+ },
sound_note: {
where: 'device',
- default: { type: 'syuilo/n-aec', volume: 0 },
+ default: { type: 'syuilo/n-aec', volume: 0 } as SoundStore,
},
sound_noteMy: {
where: 'device',
- default: { type: 'syuilo/n-cea-4va', volume: 1 },
+ default: { type: 'syuilo/n-cea-4va', volume: 1 } as SoundStore,
},
sound_notification: {
where: 'device',
- default: { type: 'syuilo/n-ea', volume: 1 },
+ default: { type: 'syuilo/n-ea', volume: 1 } as SoundStore,
},
sound_antenna: {
where: 'device',
- default: { type: 'syuilo/triple', volume: 1 },
+ default: { type: 'syuilo/triple', volume: 1 } as SoundStore,
},
sound_channel: {
where: 'device',
- default: { type: 'syuilo/square-pico', volume: 1 },
+ default: { type: 'syuilo/square-pico', volume: 1 } as SoundStore,
+ },
+ sound_reaction: {
+ where: 'device',
+ default: { type: 'syuilo/bubble2', volume: 1 } as SoundStore,
},
}));
diff --git a/packages/frontend/src/style.scss b/packages/frontend/src/style.scss
index b863f61cb4..fd2716bf9d 100644
--- a/packages/frontend/src/style.scss
+++ b/packages/frontend/src/style.scss
@@ -159,6 +159,10 @@ hr {
background: var(--divider);
}
+rt {
+ white-space: initial;
+}
+
.ph-bold {
width: 1.28em;
vertical-align: -12%;
diff --git a/packages/frontend/src/types/menu.ts b/packages/frontend/src/types/menu.ts
index 66061fcd70..f4516bbe5b 100644
--- a/packages/frontend/src/types/menu.ts
+++ b/packages/frontend/src/types/menu.ts
@@ -8,13 +8,13 @@ import { Ref } from 'vue';
export type MenuAction = (ev: MouseEvent) => void;
-export type MenuDivider = null;
+export type MenuDivider = { type: 'divider' };
export type MenuNull = undefined;
export type MenuLabel = { type: 'label', text: string };
export type MenuLink = { type: 'link', to: string, text: string, icon?: string, indicate?: boolean, avatar?: Misskey.entities.User };
export type MenuA = { type: 'a', href: string, target?: string, download?: string, text: string, icon?: string, indicate?: boolean };
export type MenuUser = { type: 'user', user: Misskey.entities.User, active?: boolean, indicate?: boolean, action: MenuAction };
-export type MenuSwitch = { type: 'switch', ref: Ref<boolean>, text: string, disabled?: boolean };
+export type MenuSwitch = { type: 'switch', ref: Ref<boolean>, text: string, disabled?: boolean | Ref<boolean> };
export type MenuButton = { type?: 'button', text: string, icon?: string, indicate?: boolean, danger?: boolean, active?: boolean, avatar?: Misskey.entities.User; action: MenuAction };
export type MenuParent = { type: 'parent', text: string, icon?: string, children: MenuItem[] | (() => Promise<MenuItem[]> | MenuItem[]) };
diff --git a/packages/frontend/src/types/page-header.ts b/packages/frontend/src/types/page-header.ts
new file mode 100644
index 0000000000..295b97a7fd
--- /dev/null
+++ b/packages/frontend/src/types/page-header.ts
@@ -0,0 +1,11 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and other misskey contributors
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+export type PageHeaderItem = {
+ text: string;
+ icon: string;
+ highlighted?: boolean;
+ handler: (ev: MouseEvent) => void;
+};
diff --git a/packages/frontend/src/ui/_common_/common.ts b/packages/frontend/src/ui/_common_/common.ts
index d153f776dd..a3adbfb1b1 100644
--- a/packages/frontend/src/ui/_common_/common.ts
+++ b/packages/frontend/src/ui/_common_/common.ts
@@ -64,7 +64,7 @@ export function openInstanceMenu(ev: MouseEvent) {
text: i18n.ts.charts,
icon: 'ph-chart-line ph-bold ph-lg',
to: '/about#charts',
- }, null, {
+ }, { type: 'divider' }, {
type: 'link',
text: i18n.ts.ads,
icon: 'ph-flag ph-bold ph-lg',
@@ -79,29 +79,29 @@ export function openInstanceMenu(ev: MouseEvent) {
text: i18n.ts.tools,
icon: 'ph-toolbox ph-bold ph-lg',
children: toolsMenuItems(),
- }, null, (instance.impressumUrl) ? {
+ }, { type: 'divider' }, (instance.impressumUrl) ? {
text: i18n.ts.impressum,
icon: 'ph-newspaper-clipping ph-bold ph-lg',
action: () => {
- window.open(instance.impressumUrl, '_blank');
+ window.open(instance.impressumUrl, '_blank', 'noopener');
},
} : undefined, (instance.tosUrl) ? {
text: i18n.ts.termsOfService,
icon: 'ph-notebook ph-bold ph-lg',
action: () => {
- window.open(instance.tosUrl, '_blank');
+ window.open(instance.tosUrl, '_blank', 'noopener');
},
} : undefined, (instance.privacyPolicyUrl) ? {
text: i18n.ts.privacyPolicy,
icon: 'ph-shield ph-bold ph-lg',
action: () => {
- window.open(instance.privacyPolicyUrl, '_blank');
+ window.open(instance.privacyPolicyUrl, '_blank', 'noopener');
},
- } : undefined, (!instance.impressumUrl && !instance.tosUrl && !instance.privacyPolicyUrl) ? undefined : null, {
+ } : undefined, (!instance.impressumUrl && !instance.tosUrl && !instance.privacyPolicyUrl) ? undefined : { type: 'divider' }, {
text: i18n.ts.help,
icon: 'ph-question ph-bold ph-lg',
action: () => {
- window.open('https://misskey-hub.net/help.html', '_blank');
+ window.open('https://misskey-hub.net/docs/for-users/', '_blank', 'noopener');
},
}, ($i) ? {
text: i18n.ts._initialTutorial.launchTutorial,
diff --git a/packages/frontend/src/ui/_common_/common.vue b/packages/frontend/src/ui/_common_/common.vue
index 7f8556d8d2..6b69e1accf 100644
--- a/packages/frontend/src/ui/_common_/common.vue
+++ b/packages/frontend/src/ui/_common_/common.vue
@@ -63,7 +63,7 @@ const XUpload = defineAsyncComponent(() => import('./upload.vue'));
const dev = _DEV_;
-let notifications = $ref<Misskey.entities.Notification[]>([]);
+const notifications = ref<Misskey.entities.Notification[]>([]);
function onNotification(notification: Misskey.entities.Notification, isClient = false) {
if (document.visibilityState === 'visible') {
@@ -72,13 +72,13 @@ function onNotification(notification: Misskey.entities.Notification, isClient =
useStream().send('readNotification');
}
- notifications.unshift(notification);
+ notifications.value.unshift(notification);
window.setTimeout(() => {
- if (notifications.length > 3) notifications.pop();
+ if (notifications.value.length > 3) notifications.value.pop();
}, 500);
window.setTimeout(() => {
- notifications = notifications.filter(x => x.id !== notification.id);
+ notifications.value = notifications.value.filter(x => x.id !== notification.id);
}, 6000);
}
diff --git a/packages/frontend/src/ui/_common_/statusbar-federation.vue b/packages/frontend/src/ui/_common_/statusbar-federation.vue
index ff980e0d88..c92695afed 100644
--- a/packages/frontend/src/ui/_common_/statusbar-federation.vue
+++ b/packages/frontend/src/ui/_common_/statusbar-federation.vue
@@ -47,9 +47,9 @@ const props = defineProps<{
refreshIntervalSec?: number;
}>();
-const instances = ref<Misskey.entities.Instance[]>([]);
+const instances = ref<Misskey.entities.FederationInstance[]>([]);
const fetching = ref(true);
-let key = $ref(0);
+const key = ref(0);
const tick = () => {
os.api('federation/instances', {
@@ -58,7 +58,7 @@ const tick = () => {
}).then(res => {
instances.value = res;
fetching.value = false;
- key++;
+ key.value++;
});
};
diff --git a/packages/frontend/src/ui/_common_/statusbar-rss.vue b/packages/frontend/src/ui/_common_/statusbar-rss.vue
index c8e5b3a8af..58e109ad7f 100644
--- a/packages/frontend/src/ui/_common_/statusbar-rss.vue
+++ b/packages/frontend/src/ui/_common_/statusbar-rss.vue
@@ -44,7 +44,7 @@ const props = defineProps<{
const items = ref([]);
const fetching = ref(true);
-let key = $ref(0);
+const key = ref(0);
const tick = () => {
window.fetch(`/api/fetch-rss?url=${props.url}`, {}).then(res => {
@@ -54,7 +54,7 @@ const tick = () => {
}
items.value = feed.items;
fetching.value = false;
- key++;
+ key.value++;
});
});
};
diff --git a/packages/frontend/src/ui/_common_/statusbar-user-list.vue b/packages/frontend/src/ui/_common_/statusbar-user-list.vue
index f1fcd315d0..6057174ba8 100644
--- a/packages/frontend/src/ui/_common_/statusbar-user-list.vue
+++ b/packages/frontend/src/ui/_common_/statusbar-user-list.vue
@@ -50,7 +50,7 @@ const props = defineProps<{
const notes = ref<Misskey.entities.Note[]>([]);
const fetching = ref(true);
-let key = $ref(0);
+const key = ref(0);
const tick = () => {
if (props.userListId == null) return;
@@ -59,7 +59,7 @@ const tick = () => {
}).then(res => {
notes.value = res;
fetching.value = false;
- key++;
+ key.value++;
});
};
diff --git a/packages/frontend/src/ui/_common_/stream-indicator.vue b/packages/frontend/src/ui/_common_/stream-indicator.vue
index 0955a71718..4cb773d28a 100644
--- a/packages/frontend/src/ui/_common_/stream-indicator.vue
+++ b/packages/frontend/src/ui/_common_/stream-indicator.vue
@@ -14,7 +14,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { onUnmounted } from 'vue';
+import { onUnmounted, ref } from 'vue';
import { useStream } from '@/stream.js';
import { i18n } from '@/i18n.js';
import MkButton from '@/components/MkButton.vue';
@@ -23,14 +23,14 @@ import { defaultStore } from '@/store.js';
const zIndex = os.claimZIndex('high');
-let hasDisconnected = $ref(false);
+const hasDisconnected = ref(false);
function onDisconnected() {
- hasDisconnected = true;
+ hasDisconnected.value = true;
}
function resetDisconnected() {
- hasDisconnected = false;
+ hasDisconnected.value = false;
}
function reload() {
diff --git a/packages/frontend/src/ui/classic.header.vue b/packages/frontend/src/ui/classic.header.vue
index bed1b6728f..62531a4f4d 100644
--- a/packages/frontend/src/ui/classic.header.vue
+++ b/packages/frontend/src/ui/classic.header.vue
@@ -47,7 +47,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { computed, defineAsyncComponent, onMounted } from 'vue';
+import { computed, defineAsyncComponent, onMounted, ref } from 'vue';
import { openInstanceMenu } from './_common_/common';
import * as os from '@/os.js';
import { navbarItemDef } from '@/navbar';
@@ -59,12 +59,12 @@ import { i18n } from '@/i18n.js';
const WINDOW_THRESHOLD = 1400;
-let settingsWindowed = $ref(window.innerWidth > WINDOW_THRESHOLD);
-let menu = $ref(defaultStore.state.menu);
+const settingsWindowed = ref(window.innerWidth > WINDOW_THRESHOLD);
+const menu = ref(defaultStore.state.menu);
// const menuDisplay = computed(defaultStore.makeGetterSetter('menuDisplay'));
-let otherNavItemIndicated = computed<boolean>(() => {
+const otherNavItemIndicated = computed<boolean>(() => {
for (const def in navbarItemDef) {
- if (menu.includes(def)) continue;
+ if (menu.value.includes(def)) continue;
if (navbarItemDef[def].indicated) return true;
}
return false;
@@ -86,7 +86,7 @@ function openAccountMenu(ev: MouseEvent) {
onMounted(() => {
window.addEventListener('resize', () => {
- settingsWindowed = (window.innerWidth >= WINDOW_THRESHOLD);
+ settingsWindowed.value = (window.innerWidth >= WINDOW_THRESHOLD);
}, { passive: true });
});
diff --git a/packages/frontend/src/ui/classic.sidebar.vue b/packages/frontend/src/ui/classic.sidebar.vue
index 6e275c5349..4fa8f1b434 100644
--- a/packages/frontend/src/ui/classic.sidebar.vue
+++ b/packages/frontend/src/ui/classic.sidebar.vue
@@ -49,7 +49,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { defineAsyncComponent, onMounted, computed, watch, nextTick } from 'vue';
+import { defineAsyncComponent, computed, watch, ref, shallowRef } from 'vue';
import { openInstanceMenu } from './_common_/common.js';
// import { host } from '@/config.js';
import * as os from '@/os.js';
@@ -65,24 +65,24 @@ import { i18n } from '@/i18n.js';
const WINDOW_THRESHOLD = 1400;
-const menu = $ref(defaultStore.state.menu);
+const menu = ref(defaultStore.state.menu);
const menuDisplay = computed(defaultStore.makeGetterSetter('menuDisplay'));
const otherNavItemIndicated = computed<boolean>(() => {
for (const def in navbarItemDef) {
- if (menu.includes(def)) continue;
+ if (menu.value.includes(def)) continue;
if (navbarItemDef[def].indicated) return true;
}
return false;
});
-let el = $shallowRef<HTMLElement>();
+const el = shallowRef<HTMLElement>();
// let accounts = $ref([]);
// let connection = $ref(null);
-let iconOnly = $ref(false);
-let settingsWindowed = $ref(false);
+const iconOnly = ref(false);
+const settingsWindowed = ref(false);
function calcViewState() {
- iconOnly = (window.innerWidth <= WINDOW_THRESHOLD) || (menuDisplay.value === 'sideIcon');
- settingsWindowed = (window.innerWidth > WINDOW_THRESHOLD);
+ iconOnly.value = (window.innerWidth <= WINDOW_THRESHOLD) || (menuDisplay.value === 'sideIcon');
+ settingsWindowed.value = (window.innerWidth > WINDOW_THRESHOLD);
}
function more(ev: MouseEvent) {
diff --git a/packages/frontend/src/ui/classic.vue b/packages/frontend/src/ui/classic.vue
index 8f06f66012..aadeaea46f 100644
--- a/packages/frontend/src/ui/classic.vue
+++ b/packages/frontend/src/ui/classic.vue
@@ -11,7 +11,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<div v-if="!showMenuOnTop" class="sidebar">
<XSidebar/>
</div>
- <div v-else ref="widgetsLeft" class="widgets left">
+ <div v-else-if="!pageMetadata?.needWideArea" ref="widgetsLeft" class="widgets left">
<XWidgets place="left" :marginTop="'var(--margin)'" @mounted="attachSticky(widgetsLeft)"/>
</div>
@@ -21,7 +21,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</div>
</main>
- <div v-if="isDesktop" ref="widgetsRight" class="widgets right">
+ <div v-if="isDesktop && !pageMetadata?.needWideArea" ref="widgetsRight" class="widgets right">
<XWidgets :place="showMenuOnTop ? 'right' : null" :marginTop="showMenuOnTop ? '0' : 'var(--margin)'" @mounted="attachSticky(widgetsRight)"/>
</div>
</div>
@@ -46,7 +46,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { defineAsyncComponent, ComputedRef, onMounted, provide } from 'vue';
+import { defineAsyncComponent, onMounted, provide, ref, computed, shallowRef } from 'vue';
import XSidebar from './classic.sidebar.vue';
import XCommon from './_common_/common.vue';
import { instanceName } from '@/config.js';
@@ -62,26 +62,26 @@ const XWidgets = defineAsyncComponent(() => import('./universal.widgets.vue'));
const DESKTOP_THRESHOLD = 1100;
-let isDesktop = $ref(window.innerWidth >= DESKTOP_THRESHOLD);
+const isDesktop = ref(window.innerWidth >= DESKTOP_THRESHOLD);
-let pageMetadata = $ref<null | ComputedRef<PageMetadata>>();
-let widgetsShowing = $ref(false);
-let fullView = $ref(false);
-let globalHeaderHeight = $ref(0);
+const pageMetadata = ref<null | PageMetadata>();
+const widgetsShowing = ref(false);
+const fullView = ref(false);
+const globalHeaderHeight = ref(0);
const wallpaper = miLocalStorage.getItem('wallpaper') != null;
-const showMenuOnTop = $computed(() => defaultStore.state.menuDisplay === 'top');
-let live2d = $shallowRef<HTMLIFrameElement>();
-let widgetsLeft = $ref();
-let widgetsRight = $ref();
+const showMenuOnTop = computed(() => defaultStore.state.menuDisplay === 'top');
+const live2d = shallowRef<HTMLIFrameElement>();
+const widgetsLeft = ref();
+const widgetsRight = ref();
provide('router', mainRouter);
provideMetadataReceiver((info) => {
- pageMetadata = info;
+ pageMetadata.value = info.value;
if (pageMetadata.value) {
document.title = `${pageMetadata.value.title} | ${instanceName}`;
}
});
-provide('shouldHeaderThin', showMenuOnTop);
+provide('shouldHeaderThin', showMenuOnTop.value);
provide('forceSpacerMin', true);
function attachSticky(el) {
@@ -110,10 +110,10 @@ function onContextmenu(ev: MouseEvent) {
type: 'label',
text: path,
}, {
- icon: fullView ? 'ph-arrows-in-simple ph-bold ph-lg' : 'ph-frame-corners ph-bold ph-lg',
- text: fullView ? i18n.ts.quitFullView : i18n.ts.fullView,
+ icon: fullView.value ? 'ph-arrows-in-simple ph-bold ph-lg' : 'ph-frame-corners ph-bold ph-lg',
+ text: fullView.value ? i18n.ts.quitFullView : i18n.ts.fullView,
action: () => {
- fullView = !fullView;
+ fullView.value = !fullView.value;
},
}, {
icon: 'ph-frame-corners ph-bold ph-lg',
@@ -154,13 +154,13 @@ defaultStore.loaded.then(() => {
onMounted(() => {
window.addEventListener('resize', () => {
- isDesktop = (window.innerWidth >= DESKTOP_THRESHOLD);
+ isDesktop.value = (window.innerWidth >= DESKTOP_THRESHOLD);
}, { passive: true });
if (defaultStore.state.aiChanMode) {
- const iframeRect = live2d.getBoundingClientRect();
+ const iframeRect = live2d.value.getBoundingClientRect();
window.addEventListener('mousemove', ev => {
- live2d.contentWindow.postMessage({
+ live2d.value.contentWindow.postMessage({
type: 'moveCursor',
body: {
x: ev.clientX - iframeRect.left,
@@ -169,7 +169,7 @@ onMounted(() => {
}, '*');
}, { passive: true });
window.addEventListener('touchmove', ev => {
- live2d.contentWindow.postMessage({
+ live2d.value.contentWindow.postMessage({
type: 'moveCursor',
body: {
x: ev.touches[0].clientX - iframeRect.left,
diff --git a/packages/frontend/src/ui/deck.vue b/packages/frontend/src/ui/deck.vue
index 3224944cd6..0df814fc88 100644
--- a/packages/frontend/src/ui/deck.vue
+++ b/packages/frontend/src/ui/deck.vue
@@ -8,7 +8,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<XSidebar v-if="!isMobile"/>
<div :class="$style.main">
- <XAnnouncements v-if="$i" :class="$style.announcements"/>
+ <XAnnouncements v-if="$i"/>
<XStatusBars/>
<div ref="columnsEl" :class="[$style.sections, { [$style.center]: deckStore.reactiveState.columnAlign.value === 'center', [$style.snapScroll]: snapScroll }]" @contextmenu.self.prevent="onContextmenu" @wheel.self="onWheel">
<!-- sectionを利用しているのは、deck.vue側でcolumnに対してfirst-of-typeを効かせるため -->
@@ -92,14 +92,13 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { computed, defineAsyncComponent, ref, watch } from 'vue';
+import { computed, defineAsyncComponent, ref, watch, shallowRef } from 'vue';
import { v4 as uuid } from 'uuid';
import XCommon from './_common_/common.vue';
import { deckStore, addColumn as addColumnToStore, loadDeck, getProfiles, deleteProfile as deleteProfile_ } from './deck/deck-store.js';
import XSidebar from '@/ui/_common_/navbar.vue';
import XDrawerMenu from '@/ui/_common_/navbar-for-mobile.vue';
import MkButton from '@/components/MkButton.vue';
-import { getScrollContainer } from '@/scripts/scroll.js';
import * as os from '@/os.js';
import { navbarItemDef } from '@/navbar.js';
import { $i } from '@/account.js';
@@ -171,7 +170,7 @@ function showSettings() {
os.pageWindow('/settings/deck');
}
-let columnsEl = $shallowRef<HTMLElement>();
+const columnsEl = shallowRef<HTMLElement>();
const addColumn = async (ev) => {
const columns = [
@@ -212,7 +211,7 @@ const onContextmenu = (ev) => {
function onWheel(ev: WheelEvent) {
if (ev.deltaX === 0) {
- columnsEl.scrollLeft += ev.deltaY;
+ columnsEl.value.scrollLeft += ev.deltaY;
}
}
@@ -236,7 +235,7 @@ function changeProfile(ev: MouseEvent) {
deckStore.set('profile', k);
unisonReload();
},
- }))), null, {
+ }))), { type: 'divider' }, {
text: i18n.ts._deck.newProfile,
icon: 'ph-plus ph-bold ph-lg',
action: async () => {
diff --git a/packages/frontend/src/ui/deck/antenna-column.vue b/packages/frontend/src/ui/deck/antenna-column.vue
index d139d975fc..7cd1d6aee9 100644
--- a/packages/frontend/src/ui/deck/antenna-column.vue
+++ b/packages/frontend/src/ui/deck/antenna-column.vue
@@ -14,7 +14,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { onMounted } from 'vue';
+import { onMounted, shallowRef } from 'vue';
import XColumn from './column.vue';
import { updateColumn, Column } from './deck-store.js';
import MkTimeline from '@/components/MkTimeline.vue';
@@ -26,7 +26,7 @@ const props = defineProps<{
isStacked: boolean;
}>();
-let timeline = $shallowRef<InstanceType<typeof MkTimeline>>();
+const timeline = shallowRef<InstanceType<typeof MkTimeline>>();
onMounted(() => {
if (props.column.antennaId == null) {
diff --git a/packages/frontend/src/ui/deck/channel-column.vue b/packages/frontend/src/ui/deck/channel-column.vue
index de6f336e09..95ed900f7d 100644
--- a/packages/frontend/src/ui/deck/channel-column.vue
+++ b/packages/frontend/src/ui/deck/channel-column.vue
@@ -19,6 +19,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
+import { shallowRef } from 'vue';
import * as Misskey from 'misskey-js';
import XColumn from './column.vue';
import { updateColumn, Column } from './deck-store.js';
@@ -32,8 +33,8 @@ const props = defineProps<{
isStacked: boolean;
}>();
-let timeline = $shallowRef<InstanceType<typeof MkTimeline>>();
-let channel = $shallowRef<Misskey.entities.Channel>();
+const timeline = shallowRef<InstanceType<typeof MkTimeline>>();
+const channel = shallowRef<Misskey.entities.Channel>();
if (props.column.channelId == null) {
setChannel();
@@ -58,14 +59,14 @@ async function setChannel() {
}
async function post() {
- if (!channel || channel.id !== props.column.channelId) {
- channel = await os.api('channels/show', {
+ if (!channel.value || channel.value.id !== props.column.channelId) {
+ channel.value = await os.api('channels/show', {
channelId: props.column.channelId,
});
}
os.post({
- channel,
+ channel: channel.value,
});
}
diff --git a/packages/frontend/src/ui/deck/column.vue b/packages/frontend/src/ui/deck/column.vue
index 09e91360b6..77e074f2ff 100644
--- a/packages/frontend/src/ui/deck/column.vue
+++ b/packages/frontend/src/ui/deck/column.vue
@@ -42,7 +42,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { onBeforeUnmount, onMounted, provide, watch } from 'vue';
+import { onBeforeUnmount, onMounted, provide, watch, shallowRef, ref, computed } from 'vue';
import { updateColumn, swapLeftColumn, swapRightColumn, swapUpColumn, swapDownColumn, stackLeftColumn, popRightColumn, removeColumn, swapColumn, Column } from './deck-store';
import * as os from '@/os.js';
import { i18n } from '@/i18n.js';
@@ -67,16 +67,16 @@ const emit = defineEmits<{
(ev: 'headerWheel', ctx: WheelEvent): void;
}>();
-let body = $shallowRef<HTMLDivElement | null>();
+const body = shallowRef<HTMLDivElement | null>();
-let dragging = $ref(false);
-watch($$(dragging), v => os.deckGlobalEvents.emit(v ? 'column.dragStart' : 'column.dragEnd'));
+const dragging = ref(false);
+watch(dragging, v => os.deckGlobalEvents.emit(v ? 'column.dragStart' : 'column.dragEnd'));
-let draghover = $ref(false);
-let dropready = $ref(false);
+const draghover = ref(false);
+const dropready = ref(false);
-const isMainColumn = $computed(() => props.column.type === 'main');
-const active = $computed(() => props.column.active !== false);
+const isMainColumn = computed(() => props.column.type === 'main');
+const active = computed(() => props.column.active !== false);
onMounted(() => {
os.deckGlobalEvents.on('column.dragStart', onOtherDragStart);
@@ -89,11 +89,11 @@ onBeforeUnmount(() => {
});
function onOtherDragStart() {
- dropready = true;
+ dropready.value = true;
}
function onOtherDragEnd() {
- dropready = false;
+ dropready.value = false;
}
function toggleActive() {
@@ -104,7 +104,7 @@ function toggleActive() {
}
function getMenu() {
- let items = [{
+ let items: MenuItem[] = [{
icon: 'ph-gear ph-bold ph-lg',
text: i18n.ts._deck.configureColumn,
action: async () => {
@@ -170,7 +170,7 @@ function getMenu() {
action: () => {
popRightColumn(props.column.id);
},
- } : undefined, null, {
+ } : undefined, { type: 'divider' }, {
icon: 'ph-trash ph-bold ph-lg',
text: i18n.ts.remove,
danger: true,
@@ -180,7 +180,7 @@ function getMenu() {
}];
if (props.menu) {
- items.unshift(null);
+ items.unshift({ type: 'divider' });
items = props.menu.concat(items);
}
@@ -208,8 +208,8 @@ function onContextmenu(ev: MouseEvent) {
}
function goTop() {
- if (body) {
- body.scrollTo({
+ if (body.value) {
+ body.value.scrollTo({
top: 0,
behavior: 'smooth',
});
@@ -223,17 +223,17 @@ function onDragstart(ev) {
// Chromeのバグで、Dragstartハンドラ内ですぐにDOMを変更する(=リアクティブなプロパティを変更する)とDragが終了してしまう
// SEE: https://stackoverflow.com/questions/19639969/html5-dragend-event-firing-immediately
window.setTimeout(() => {
- dragging = true;
+ dragging.value = true;
}, 10);
}
function onDragend(ev) {
- dragging = false;
+ dragging.value = false;
}
function onDragover(ev) {
// 自分自身がドラッグされている場合
- if (dragging) {
+ if (dragging.value) {
// 自分自身にはドロップさせない
ev.dataTransfer.dropEffect = 'none';
} else {
@@ -241,16 +241,16 @@ function onDragover(ev) {
ev.dataTransfer.dropEffect = isDeckColumn ? 'move' : 'none';
- if (isDeckColumn) draghover = true;
+ if (isDeckColumn) draghover.value = true;
}
}
function onDragleave() {
- draghover = false;
+ draghover.value = false;
}
function onDrop(ev) {
- draghover = false;
+ draghover.value = false;
os.deckGlobalEvents.emit('column.dragEnd');
const id = ev.dataTransfer.getData(_DATA_TRANSFER_DECK_COLUMN_);
diff --git a/packages/frontend/src/ui/deck/direct-column.vue b/packages/frontend/src/ui/deck/direct-column.vue
index bbdf1b86e9..e2a212be46 100644
--- a/packages/frontend/src/ui/deck/direct-column.vue
+++ b/packages/frontend/src/ui/deck/direct-column.vue
@@ -12,7 +12,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { } from 'vue';
+import { ref } from 'vue';
import XColumn from './column.vue';
import { Column } from './deck-store.js';
import MkNotes from '@/components/MkNotes.vue';
@@ -30,11 +30,11 @@ const pagination = {
},
};
-const tlComponent: InstanceType<typeof MkNotes> = $ref();
+const tlComponent = ref<InstanceType<typeof MkNotes>>();
function reloadTimeline() {
return new Promise<void>((res) => {
- tlComponent.pagingComponent?.reload().then(() => {
+ tlComponent.value.pagingComponent?.reload().then(() => {
res();
});
});
diff --git a/packages/frontend/src/ui/deck/list-column.vue b/packages/frontend/src/ui/deck/list-column.vue
index 0e1f7d62bd..3e0ec6aac0 100644
--- a/packages/frontend/src/ui/deck/list-column.vue
+++ b/packages/frontend/src/ui/deck/list-column.vue
@@ -14,7 +14,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { watch } from 'vue';
+import { watch, shallowRef, ref } from 'vue';
import XColumn from './column.vue';
import { updateColumn, Column } from './deck-store';
import MkTimeline from '@/components/MkTimeline.vue';
@@ -26,14 +26,14 @@ const props = defineProps<{
isStacked: boolean;
}>();
-let timeline = $shallowRef<InstanceType<typeof MkTimeline>>();
-const withRenotes = $ref(props.column.withRenotes ?? true);
+const timeline = shallowRef<InstanceType<typeof MkTimeline>>();
+const withRenotes = ref(props.column.withRenotes ?? true);
if (props.column.listId == null) {
setList();
}
-watch($$(withRenotes), v => {
+watch(withRenotes, v => {
updateColumn(props.column.id, {
withRenotes: v,
});
@@ -72,7 +72,7 @@ const menu = [
{
type: 'switch',
text: i18n.ts.showRenotes,
- ref: $$(withRenotes),
+ ref: withRenotes,
},
];
</script>
diff --git a/packages/frontend/src/ui/deck/main-column.vue b/packages/frontend/src/ui/deck/main-column.vue
index 40639d46aa..4e17cb80fd 100644
--- a/packages/frontend/src/ui/deck/main-column.vue
+++ b/packages/frontend/src/ui/deck/main-column.vue
@@ -19,7 +19,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { ComputedRef, provide, shallowRef } from 'vue';
+import { ComputedRef, provide, shallowRef, ref } from 'vue';
import XColumn from './column.vue';
import { deckStore, Column } from '@/ui/deck/deck-store.js';
import * as os from '@/os.js';
@@ -35,11 +35,11 @@ defineProps<{
}>();
const contents = shallowRef<HTMLElement>();
-let pageMetadata = $ref<null | ComputedRef<PageMetadata>>();
+const pageMetadata = ref<null | ComputedRef<PageMetadata>>();
provide('router', mainRouter);
provideMetadataReceiver((info) => {
- pageMetadata = info;
+ pageMetadata.value = info;
});
/*
diff --git a/packages/frontend/src/ui/deck/mentions-column.vue b/packages/frontend/src/ui/deck/mentions-column.vue
index bb9b174cb7..7df07fd8d7 100644
--- a/packages/frontend/src/ui/deck/mentions-column.vue
+++ b/packages/frontend/src/ui/deck/mentions-column.vue
@@ -12,7 +12,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { } from 'vue';
+import { ref } from 'vue';
import XColumn from './column.vue';
import { Column } from './deck-store.js';
import MkNotes from '@/components/MkNotes.vue';
@@ -22,11 +22,11 @@ defineProps<{
isStacked: boolean;
}>();
-const tlComponent: InstanceType<typeof MkNotes> = $ref();
+const tlComponent = ref<InstanceType<typeof MkNotes>>();
function reloadTimeline() {
return new Promise<void>((res) => {
- tlComponent.pagingComponent?.reload().then(() => {
+ tlComponent.value.pagingComponent?.reload().then(() => {
res();
});
});
diff --git a/packages/frontend/src/ui/deck/notifications-column.vue b/packages/frontend/src/ui/deck/notifications-column.vue
index ce1b26e36a..6a28bab091 100644
--- a/packages/frontend/src/ui/deck/notifications-column.vue
+++ b/packages/frontend/src/ui/deck/notifications-column.vue
@@ -12,7 +12,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { defineAsyncComponent } from 'vue';
+import { defineAsyncComponent, shallowRef } from 'vue';
import XColumn from './column.vue';
import { updateColumn, Column } from './deck-store.js';
import XNotifications from '@/components/MkNotifications.vue';
@@ -24,7 +24,7 @@ const props = defineProps<{
isStacked: boolean;
}>();
-let notificationsComponent = $shallowRef<InstanceType<typeof XNotifications>>();
+const notificationsComponent = shallowRef<InstanceType<typeof XNotifications>>();
function func() {
os.popup(defineAsyncComponent(() => import('@/components/MkNotificationSelectWindow.vue')), {
diff --git a/packages/frontend/src/ui/deck/role-timeline-column.vue b/packages/frontend/src/ui/deck/role-timeline-column.vue
index b66c3ba618..5fbd1389b7 100644
--- a/packages/frontend/src/ui/deck/role-timeline-column.vue
+++ b/packages/frontend/src/ui/deck/role-timeline-column.vue
@@ -14,7 +14,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { onMounted } from 'vue';
+import { onMounted, shallowRef } from 'vue';
import XColumn from './column.vue';
import { updateColumn, Column } from './deck-store.js';
import MkTimeline from '@/components/MkTimeline.vue';
@@ -26,7 +26,7 @@ const props = defineProps<{
isStacked: boolean;
}>();
-let timeline = $shallowRef<InstanceType<typeof MkTimeline>>();
+const timeline = shallowRef<InstanceType<typeof MkTimeline>>();
onMounted(() => {
if (props.column.roleId == null) {
diff --git a/packages/frontend/src/ui/deck/tl-column.vue b/packages/frontend/src/ui/deck/tl-column.vue
index 5237010f50..f6167b08f9 100644
--- a/packages/frontend/src/ui/deck/tl-column.vue
+++ b/packages/frontend/src/ui/deck/tl-column.vue
@@ -34,7 +34,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { onMounted, watch } from 'vue';
+import { onMounted, watch, ref, shallowRef } from 'vue';
import XColumn from './column.vue';
import { removeColumn, updateColumn, Column } from './deck-store.js';
import MkTimeline from '@/components/MkTimeline.vue';
@@ -48,29 +48,29 @@ const props = defineProps<{
isStacked: boolean;
}>();
-let disabled = $ref(false);
-let timeline = $shallowRef<InstanceType<typeof MkTimeline>>();
+const disabled = ref(false);
+const timeline = shallowRef<InstanceType<typeof MkTimeline>>();
const isLocalTimelineAvailable = (($i == null && instance.policies.ltlAvailable) || ($i != null && $i.policies.ltlAvailable));
const isGlobalTimelineAvailable = (($i == null && instance.policies.gtlAvailable) || ($i != null && $i.policies.gtlAvailable));
const isBubbleTimelineAvailable = ($i == null && instance.policies.btlAvailable) || ($i != null && $i.policies.btlAvailable);
-const withRenotes = $ref(props.column.withRenotes ?? true);
-const withReplies = $ref(props.column.withReplies ?? false);
-const onlyFiles = $ref(props.column.onlyFiles ?? false);
+const withRenotes = ref(props.column.withRenotes ?? true);
+const withReplies = ref(props.column.withReplies ?? false);
+const onlyFiles = ref(props.column.onlyFiles ?? false);
-watch($$(withRenotes), v => {
+watch(withRenotes, v => {
updateColumn(props.column.id, {
withRenotes: v,
});
});
-watch($$(withReplies), v => {
+watch(withReplies, v => {
updateColumn(props.column.id, {
withReplies: v,
});
});
-watch($$(onlyFiles), v => {
+watch(onlyFiles, v => {
updateColumn(props.column.id, {
onlyFiles: v,
});
@@ -80,7 +80,7 @@ onMounted(() => {
if (props.column.tl == null) {
setType();
} else if ($i) {
- disabled = (
+ disabled.value = (
(!((instance.policies.ltlAvailable) || ($i.policies.ltlAvailable)) && ['local', 'social'].includes(props.column.tl)) ||
(!((instance.policies.gtlAvailable) || ($i.policies.gtlAvailable)) && ['global'].includes(props.column.tl)) ||
(!((instance.policies.btlAvailable) || ($i.policies.btlAvailable)) && ['bubble'].includes(props.column.tl)));
@@ -120,15 +120,17 @@ const menu = [{
}, {
type: 'switch',
text: i18n.ts.showRenotes,
- ref: $$(withRenotes),
+ ref: withRenotes,
}, props.column.tl === 'local' || props.column.tl === 'social' ? {
type: 'switch',
text: i18n.ts.showRepliesToOthersInTimeline,
- ref: $$(withReplies),
+ ref: withReplies,
+ disabled: onlyFiles,
} : undefined, {
type: 'switch',
text: i18n.ts.fileAttachedOnly,
- ref: $$(onlyFiles),
+ ref: onlyFiles,
+ disabled: props.column.tl === 'local' || props.column.tl === 'social' ? withReplies : false,
}];
</script>
diff --git a/packages/frontend/src/ui/deck/widgets-column.vue b/packages/frontend/src/ui/deck/widgets-column.vue
index 52d0c2a825..d111f92443 100644
--- a/packages/frontend/src/ui/deck/widgets-column.vue
+++ b/packages/frontend/src/ui/deck/widgets-column.vue
@@ -15,7 +15,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { } from 'vue';
+import { ref } from 'vue';
import XColumn from './column.vue';
import { addColumnWidget, Column, removeColumnWidget, setColumnWidgets, updateColumnWidget } from './deck-store.js';
import XWidgets from '@/components/MkWidgets.vue';
@@ -26,7 +26,7 @@ const props = defineProps<{
isStacked: boolean;
}>();
-let edit = $ref(false);
+const edit = ref(false);
function addWidget(widget) {
addColumnWidget(props.column.id, widget);
@@ -45,7 +45,7 @@ function updateWidgets(widgets) {
}
function func() {
- edit = !edit;
+ edit.value = !edit.value;
}
const menu = [{
diff --git a/packages/frontend/src/ui/minimum.vue b/packages/frontend/src/ui/minimum.vue
index cc5433f81c..f32f2de3df 100644
--- a/packages/frontend/src/ui/minimum.vue
+++ b/packages/frontend/src/ui/minimum.vue
@@ -14,19 +14,19 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { provide, ComputedRef } from 'vue';
+import { provide, ComputedRef, ref } from 'vue';
import XCommon from './_common_/common.vue';
import { mainRouter } from '@/router.js';
import { PageMetadata, provideMetadataReceiver } from '@/scripts/page-metadata.js';
import { instanceName } from '@/config.js';
-let pageMetadata = $ref<null | ComputedRef<PageMetadata>>();
+const pageMetadata = ref<null | ComputedRef<PageMetadata>>();
provide('router', mainRouter);
provideMetadataReceiver((info) => {
- pageMetadata = info;
- if (pageMetadata.value) {
- document.title = `${pageMetadata.value.title} | ${instanceName}`;
+ pageMetadata.value = info;
+ if (pageMetadata.value.value) {
+ document.title = `${pageMetadata.value.value.title} | ${instanceName}`;
}
});
diff --git a/packages/frontend/src/ui/universal.vue b/packages/frontend/src/ui/universal.vue
index 448f2f81f8..1d8e26bfcc 100644
--- a/packages/frontend/src/ui/universal.vue
+++ b/packages/frontend/src/ui/universal.vue
@@ -10,7 +10,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkStickyContainer ref="contents" :class="$style.contents" style="container-type: inline-size;" @contextmenu.stop="onContextmenu">
<template #header>
<div>
- <XAnnouncements v-if="$i" :class="$style.announcements"/>
+ <XAnnouncements v-if="$i"/>
<XStatusBars :class="$style.statusbars"/>
</div>
</template>
@@ -18,11 +18,11 @@ SPDX-License-Identifier: AGPL-3.0-only
<div :class="$style.spacer"></div>
</MkStickyContainer>
- <div v-if="isDesktop" :class="$style.widgets">
+ <div v-if="isDesktop && !pageMetadata?.needWideArea" :class="$style.widgets">
<XWidgets/>
</div>
- <button v-if="!isDesktop && !isMobile" :class="$style.widgetButton" class="_button" @click="widgetsShowing = true"><i class="ph-squares-four ph-bold ph-lg"></i></button>
+ <button v-if="(!isDesktop || pageMetadata?.needWideArea) && !isMobile" :class="$style.widgetButton" class="_button" @click="widgetsShowing = true"><i class="ph-squares-four ph-bold ph-lg"></i></button>
<div v-if="isMobile" ref="navFooter" :class="$style.nav">
<button :class="$style.navButton" class="_button" @click="drawerMenuShowing = true"><i :class="$style.navButtonIcon" class="ph-list ph-bold ph-lg-2"></i><span v-if="menuIndicated" :class="$style.navButtonIndicator"><i class="_indicatorCircle"></i></span></button>
@@ -95,7 +95,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { defineAsyncComponent, provide, onMounted, computed, ref, ComputedRef, watch, shallowRef, Ref } from 'vue';
+import { defineAsyncComponent, provide, onMounted, computed, ref, watch, shallowRef, Ref } from 'vue';
import XCommon from './_common_/common.vue';
import type MkStickyContainer from '@/components/global/MkStickyContainer.vue';
import { instanceName } from '@/config.js';
@@ -127,14 +127,14 @@ window.addEventListener('resize', () => {
isMobile.value = deviceKind === 'smartphone' || window.innerWidth <= MOBILE_THRESHOLD;
});
-let pageMetadata = $ref<null | ComputedRef<PageMetadata>>();
-const widgetsShowing = $ref(false);
-const navFooter = $shallowRef<HTMLElement>();
+const pageMetadata = ref<null | PageMetadata>();
+const widgetsShowing = ref(false);
+const navFooter = shallowRef<HTMLElement>();
const contents = shallowRef<InstanceType<typeof MkStickyContainer>>();
provide('router', mainRouter);
provideMetadataReceiver((info) => {
- pageMetadata = info;
+ pageMetadata.value = info.value;
if (pageMetadata.value) {
document.title = `${pageMetadata.value.title} | ${instanceName}`;
}
@@ -216,16 +216,16 @@ function top() {
});
}
-let navFooterHeight = $ref(0);
-provide<Ref<number>>(CURRENT_STICKY_BOTTOM, $$(navFooterHeight));
+const navFooterHeight = ref(0);
+provide<Ref<number>>(CURRENT_STICKY_BOTTOM, navFooterHeight);
-watch($$(navFooter), () => {
- if (navFooter) {
- navFooterHeight = navFooter.offsetHeight;
- document.body.style.setProperty('--stickyBottom', `${navFooterHeight}px`);
+watch(navFooter, () => {
+ if (navFooter.value) {
+ navFooterHeight.value = navFooter.value.offsetHeight;
+ document.body.style.setProperty('--stickyBottom', `${navFooterHeight.value}px`);
document.body.style.setProperty('--minBottomSpacing', 'var(--minBottomSpacingMobile)');
} else {
- navFooterHeight = 0;
+ navFooterHeight.value = 0;
document.body.style.setProperty('--stickyBottom', '0px');
document.body.style.setProperty('--minBottomSpacing', '0px');
}
diff --git a/packages/frontend/src/ui/universal.widgets.vue b/packages/frontend/src/ui/universal.widgets.vue
index 08ed6d324c..57d6ae0330 100644
--- a/packages/frontend/src/ui/universal.widgets.vue
+++ b/packages/frontend/src/ui/universal.widgets.vue
@@ -13,10 +13,10 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts">
-let editMode = $ref(false);
+import { computed, ref } from 'vue';
+const editMode = ref(false);
</script>
<script lang="ts" setup>
-import { } from 'vue';
import XWidgets from '@/components/MkWidgets.vue';
import { i18n } from '@/i18n.js';
import { defaultStore } from '@/store.js';
@@ -30,7 +30,7 @@ const props = withDefaults(defineProps<{
place: null,
});
-const widgets = $computed(() => {
+const widgets = computed(() => {
if (props.place === null) return defaultStore.reactiveState.widgets.value;
if (props.place === 'left') return defaultStore.reactiveState.widgets.value.filter(w => w.place === 'left');
return defaultStore.reactiveState.widgets.value.filter(w => w.place !== 'left');
diff --git a/packages/frontend/src/ui/visitor.vue b/packages/frontend/src/ui/visitor.vue
index 8d795e6dcb..459ba946b6 100644
--- a/packages/frontend/src/ui/visitor.vue
+++ b/packages/frontend/src/ui/visitor.vue
@@ -67,9 +67,9 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { ComputedRef, onMounted, provide } from 'vue';
+import { ComputedRef, onMounted, provide, ref, computed } from 'vue';
import XCommon from './_common_/common.vue';
-import { host, instanceName } from '@/config.js';
+import { instanceName } from '@/config.js';
import * as os from '@/os.js';
import { instance } from '@/instance.js';
import XSigninDialog from '@/components/MkSigninDialog.vue';
@@ -82,13 +82,13 @@ import MkVisitorDashboard from '@/components/MkVisitorDashboard.vue';
const DESKTOP_THRESHOLD = 1100;
-let pageMetadata = $ref<null | ComputedRef<PageMetadata>>();
+const pageMetadata = ref<null | ComputedRef<PageMetadata>>();
provide('router', mainRouter);
provideMetadataReceiver((info) => {
- pageMetadata = info;
- if (pageMetadata.value) {
- document.title = `${pageMetadata.value.title} | ${instanceName}`;
+ pageMetadata.value = info;
+ if (pageMetadata.value.value) {
+ document.title = `${pageMetadata.value.value.title} | ${instanceName}`;
}
});
@@ -97,14 +97,14 @@ const announcements = {
limit: 10,
};
-const isTimelineAvailable = $ref(instance.policies?.ltlAvailable || instance.policies?.gtlAvailable);
+const isTimelineAvailable = ref(instance.policies?.ltlAvailable || instance.policies?.gtlAvailable);
-let showMenu = $ref(false);
-let isDesktop = $ref(window.innerWidth >= DESKTOP_THRESHOLD);
-let narrow = $ref(window.innerWidth < 1280);
-let meta = $ref();
+const showMenu = ref(false);
+const isDesktop = ref(window.innerWidth >= DESKTOP_THRESHOLD);
+const narrow = ref(window.innerWidth < 1280);
+const meta = ref();
-const keymap = $computed(() => {
+const keymap = computed(() => {
return {
'd': () => {
if (ColdDeviceStorage.get('syncDeviceDarkMode')) return;
@@ -116,10 +116,10 @@ const keymap = $computed(() => {
};
});
-const root = $computed(() => mainRouter.currentRoute.value.name === 'index');
+const root = computed(() => mainRouter.currentRoute.value.name === 'index');
os.api('meta', { detail: true }).then(res => {
- meta = res;
+ meta.value = res;
});
function signin() {
@@ -135,15 +135,15 @@ function signup() {
}
onMounted(() => {
- if (!isDesktop) {
+ if (!isDesktop.value) {
window.addEventListener('resize', () => {
- if (window.innerWidth >= DESKTOP_THRESHOLD) isDesktop = true;
+ if (window.innerWidth >= DESKTOP_THRESHOLD) isDesktop.value = true;
}, { passive: true });
}
});
defineExpose({
- showMenu: $$(showMenu),
+ showMenu: showMenu,
});
</script>
diff --git a/packages/frontend/src/ui/zen.vue b/packages/frontend/src/ui/zen.vue
index a4f75136ec..9f92f78764 100644
--- a/packages/frontend/src/ui/zen.vue
+++ b/packages/frontend/src/ui/zen.vue
@@ -22,22 +22,22 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { provide, ComputedRef } from 'vue';
+import { provide, ComputedRef, ref } from 'vue';
import XCommon from './_common_/common.vue';
import { mainRouter } from '@/router.js';
import { PageMetadata, provideMetadataReceiver } from '@/scripts/page-metadata.js';
import { instanceName, ui } from '@/config.js';
import { i18n } from '@/i18n.js';
-let pageMetadata = $ref<null | ComputedRef<PageMetadata>>();
+const pageMetadata = ref<null | ComputedRef<PageMetadata>>();
const showBottom = !(new URLSearchParams(location.search)).has('zen') && ui === 'deck';
provide('router', mainRouter);
provideMetadataReceiver((info) => {
- pageMetadata = info;
- if (pageMetadata.value) {
- document.title = `${pageMetadata.value.title} | ${instanceName}`;
+ pageMetadata.value = info;
+ if (pageMetadata.value.value) {
+ document.title = `${pageMetadata.value.value.title} | ${instanceName}`;
}
});
diff --git a/packages/frontend/src/unicode-emoji-indexes/en-US.json b/packages/frontend/src/unicode-emoji-indexes/en-US.json
index 567125c4c7..4d8b040ad2 100644
--- a/packages/frontend/src/unicode-emoji-indexes/en-US.json
+++ b/packages/frontend/src/unicode-emoji-indexes/en-US.json
@@ -103,6 +103,7 @@
"🫥": ["depressed", "disappear", "hide", "introvert", "invisible", "tensen"],
"🫤": ["disappointed", "meh", "skeptical", "unsure"],
"🥹": ["angry", "cry", "proud", "resist", "sad"],
+ "🫨": ["earthquake", "face", "shaking", "shock", "vibrate"],
"💩": ["hankey", "shitface", "fail", "turd", "shit"],
"😈": ["devil", "horns"],
"👿": ["devil", "angry", "horns"],
@@ -132,6 +133,8 @@
"✊": ["fingers", "hand", "grasp"],
"🤛": ["hand", "fistbump"],
"🤜": ["hand", "fistbump"],
+ "🫷": ["hand", "high_five", "leftward", "push", "refuse", "stop", "wait"],
+ "🫸": ["hand", "high_five", "push", "refuse", "rightward", "stop", "wait"],
"✌": ["fingers", "ohyeah", "hand", "peace", "victory", "two"],
"👌": ["fingers", "limbs", "perfect", "ok", "okay"],
"✋": ["fingers", "stop", "highfive", "palm", "ban"],
@@ -453,6 +456,7 @@
"🐸": ["animal", "nature", "croak", "toad"],
"🦑": ["animal", "nature", "ocean", "sea"],
"🐙": ["animal", "creature", "ocean", "sea", "nature", "beach"],
+ "🪼": ["animal", "creature", "ocean", "sea", "nature", "beach"],
"🦐": ["animal", "ocean", "nature", "seafood"],
"🐵": ["animal", "nature", "circus"],
"🦍": ["animal", "nature", "circus"],
@@ -466,7 +470,9 @@
"🐤": ["animal", "chicken", "bird"],
"🐣": ["animal", "chicken", "egg", "born", "baby", "bird"],
"🐥": ["animal", "chicken", "baby", "bird"],
+ "🪿": ["animal", "nature", "bird", "fowl", "goose", "honk", "silly"],
"🦆": ["animal", "nature", "bird", "mallard"],
+ "🐦‍⬛": ["animal", "nature", "bird", "black", "crow", "raven", "rook"],
"🦅": ["animal", "nature", "bird"],
"🦉": ["animal", "nature", "bird", "hoot"],
"🦇": ["animal", "nature", "blind", "vampire"],
@@ -474,9 +480,11 @@
"🐗": ["animal", "nature"],
"🐴": ["animal", "brown", "nature"],
"🦄": ["animal", "nature", "mystical"],
+ "🫎": ["animal", "nature", "antlers", "elk", "mammal"],
"🐝": ["animal", "insect", "nature", "bug", "spring", "honey"],
"🐛": ["animal", "insect", "nature", "worm"],
"🦋": ["animal", "insect", "nature", "caterpillar"],
+ "🫏": ["animal", "ass", "burro", "mammal", "mule", "stubborn"],
"🐌": ["slow", "animal", "shell"],
"🐞": ["animal", "insect", "nature", "ladybug"],
"🐜": ["animal", "insect", "nature", "bug"],
@@ -546,6 +554,7 @@
"🐻‍❄️": ["animal", "nature"],
"🦤": ["animal", "nature"],
"🪶": ["animal", "nature"],
+ "🪽": ["angelic", "aviation", "bird", "flying", "mythology"],
"🦭": ["animal", "nature"],
"🐾": ["animal", "tracking", "footprints", "dog", "cat", "pet", "feet"],
"🐉": ["animal", "myth", "nature", "chinese", "green"],
@@ -576,6 +585,7 @@
"🌻": ["nature", "plant", "fall"],
"🌹": ["flowers", "valentines", "love", "spring"],
"🥀": ["plant", "nature", "flower"],
+ "🪻": ["plant", "nature", "flower", "bluebonnet", "lavender", "lupine", "snapdragon"],
"🌷": ["flowers", "plant", "nature", "summer", "spring"],
"🌼": ["nature", "flowers", "yellow"],
"🌸": ["nature", "plant", "spring", "flower"],
@@ -655,6 +665,7 @@
"🥝": ["fruit", "food"],
"🥭": ["fruit", "food", "tropical"],
"🥑": ["fruit", "food"],
+ "🫛": ["beans", "edamame", "legume", "pea", "pod", "vegetable", "food"],
"🥦": ["fruit", "food", "vegetable"],
"🍅": ["fruit", "vegetable", "nature", "food"],
"🍆": ["vegetable", "nature", "food", "aubergine"],
@@ -668,6 +679,7 @@
"🌽": ["food", "vegetable", "plant"],
"🥬": ["food", "vegetable", "plant", "bok choy", "cabbage", "kale", "lettuce"],
"🍠": ["food", "nature"],
+ "🫚": ["food", "nature", "beer", "root", "spice"],
"🥜": ["food", "nut"],
"🧄": ["food"],
"🧅": ["food"],
@@ -850,9 +862,11 @@
"🎧": ["music", "score", "gadgets"],
"🎼": ["treble", "clef", "compose"],
"🎹": ["piano", "instrument", "compose"],
+ "🪇": ["instrument", "music", "percussion", "rattle", "shake"],
"🥁": ["music", "instrument", "drumsticks", "snare"],
"🎷": ["music", "instrument", "jazz", "blues"],
"🎺": ["music", "brass"],
+ "🪈": ["music", "fife", "pipe", "recorder", "woodwind"],
"🎸": ["music", "instrument"],
"🎻": ["music", "instrument", "orchestra", "symphony"],
"🪕": ["music", "instrument"],
@@ -1108,6 +1122,7 @@
"🩹": ["health", "hospital", "medicine", "needle", "doctor", "nurse"],
"🩺": ["health", "hospital", "medicine", "needle", "doctor", "nurse"],
"🪒": ["health"],
+ "🪮": ["afro", "comb", "hair", "pick"],
"🩻": [],
"🩼": [],
"🧬": ["biologist", "genetics", "life"],
@@ -1156,6 +1171,7 @@
"🎊": ["festival", "party", "birthday", "circus"],
"🎉": ["party", "congratulations", "birthday", "magic", "circus", "celebration"],
"🎎": ["japanese", "toy", "kimono"],
+ "🪭": ["cooling", "dance", "fan", "flutter", "hot", "shy"],
"🎐": ["nature", "ding", "spring", "bell"],
"🎌": ["japanese", "nation", "country", "border"],
"🏮": ["light", "paper", "halloween", "spooky"],
@@ -1237,14 +1253,17 @@
"🪧": [],
"💯": ["score", "perfect", "numbers", "century", "exam", "quiz", "test", "pass", "hundred"],
"🔢": ["numbers", "blue-square"],
+ "🩷": ["love", "like", "affection", "valentines"],
"❤️": ["love", "like", "affection", "valentines"],
"🧡": ["love", "like", "affection", "valentines"],
"💛": ["love", "like", "affection", "valentines"],
"💚": ["love", "like", "affection", "valentines"],
+ "🩵": ["love", "like", "affection", "valentines"],
"💙": ["love", "like", "affection", "valentines"],
"💜": ["love", "like", "affection", "valentines"],
"🤎": ["love", "like", "affection", "valentines"],
"🖤": ["love", "like", "affection", "valentines"],
+ "🩶": ["love", "like", "affection", "valentines"],
"🤍": ["love", "like", "affection", "valentines"],
"💔": ["sad", "sorry", "break", "heart", "heartbreak"],
"❣": ["decoration", "love"],
@@ -1263,6 +1282,7 @@
"☪": ["islam"],
"🕉": ["hinduism", "buddhism", "sikhism", "jainism"],
"☸": ["hinduism", "buddhism", "sikhism", "jainism"],
+ "🪯": ["religion", "sikh"],
"✡": ["judaism"],
"🔯": ["purple-square", "religion", "jewish", "hexagram"],
"🕎": ["hanukkah", "candles", "jewish"],
@@ -1358,6 +1378,7 @@
"🛃": ["passport", "border", "blue-square"],
"🛄": ["blue-square", "airport", "transport"],
"🛅": ["blue-square", "travel"],
+ "🛜": ["blue-square", "computer", "internet", "network"],
"♿": ["blue-square", "disabled", "a11y", "accessibility"],
"🚭": ["cigarette", "blue-square", "smell", "smoke"],
"🚾": ["toilet", "restroom", "blue-square"],
@@ -1527,258 +1548,258 @@
"🕥": ["time", "late", "early", "schedule"],
"🕦": ["time", "late", "early", "schedule"],
"🕧": ["time", "late", "early", "schedule"],
- "🇦🇫": ["af", "flag", "nation", "country", "banner"],
- "🇦🇽": ["Åland", "islands", "flag", "nation", "country", "banner"],
- "🇦🇱": ["al", "flag", "nation", "country", "banner"],
- "🇩🇿": ["dz", "flag", "nation", "country", "banner"],
- "🇦🇸": ["american", "ws", "flag", "nation", "country", "banner"],
+ "🇦🇫": ["af", "afghanistan", "flag", "nation", "country", "banner"],
+ "🇦🇽": ["ax", "Åland", "aland", "islands", "flag", "nation", "country", "banner"],
+ "🇦🇱": ["al", "albania", "flag", "nation", "country", "banner"],
+ "🇩🇿": ["dz", "algeria", "flag", "nation", "country", "banner"],
+ "🇦🇸": ["as", "american", "samoa", "flag", "nation", "country", "banner"],
"🇦🇩": ["ad", "flag", "nation", "country", "banner"],
- "🇦🇴": ["ao", "flag", "nation", "country", "banner"],
- "🇦🇮": ["ai", "flag", "nation", "country", "banner"],
- "🇦🇶": ["aq", "flag", "nation", "country", "banner"],
- "🇦🇬": ["antigua", "barbuda", "flag", "nation", "country", "banner"],
- "🇦🇷": ["ar", "flag", "nation", "country", "banner"],
- "🇦🇲": ["am", "flag", "nation", "country", "banner"],
- "🇦🇼": ["aw", "flag", "nation", "country", "banner"],
- "🇦🇨": ["flag", "nation", "country", "banner"],
- "🇦🇺": ["au", "flag", "nation", "country", "banner"],
- "🇦🇹": ["at", "flag", "nation", "country", "banner"],
- "🇦🇿": ["az", "flag", "nation", "country", "banner"],
- "🇧🇸": ["bs", "flag", "nation", "country", "banner"],
- "🇧🇭": ["bh", "flag", "nation", "country", "banner"],
- "🇧🇩": ["bd", "flag", "nation", "country", "banner"],
- "🇧🇧": ["bb", "flag", "nation", "country", "banner"],
- "🇧🇾": ["by", "flag", "nation", "country", "banner"],
- "🇧🇪": ["be", "flag", "nation", "country", "banner"],
- "🇧🇿": ["bz", "flag", "nation", "country", "banner"],
- "🇧🇯": ["bj", "flag", "nation", "country", "banner"],
- "🇧🇲": ["bm", "flag", "nation", "country", "banner"],
- "🇧🇹": ["bt", "flag", "nation", "country", "banner"],
- "🇧🇴": ["bo", "flag", "nation", "country", "banner"],
- "🇧🇶": ["bonaire", "flag", "nation", "country", "banner"],
- "🇧🇦": ["bosnia", "herzegovina", "flag", "nation", "country", "banner"],
- "🇧🇼": ["bw", "flag", "nation", "country", "banner"],
- "🇧🇷": ["br", "flag", "nation", "country", "banner"],
- "🇮🇴": ["british", "indian", "ocean", "territory", "flag", "nation", "country", "banner"],
- "🇻🇬": ["british", "virgin", "islands", "bvi", "flag", "nation", "country", "banner"],
- "🇧🇳": ["bn", "darussalam", "flag", "nation", "country", "banner"],
- "🇧🇬": ["bg", "flag", "nation", "country", "banner"],
- "🇧🇫": ["burkina", "faso", "flag", "nation", "country", "banner"],
- "🇧🇮": ["bi", "flag", "nation", "country", "banner"],
- "🇨🇻": ["cabo", "verde", "flag", "nation", "country", "banner"],
- "🇰🇭": ["kh", "flag", "nation", "country", "banner"],
- "🇨🇲": ["cm", "flag", "nation", "country", "banner"],
- "🇨🇦": ["ca", "flag", "nation", "country", "banner"],
- "🇮🇨": ["canary", "islands", "flag", "nation", "country", "banner"],
- "🇰🇾": ["cayman", "islands", "flag", "nation", "country", "banner"],
- "🇨🇫": ["central", "african", "republic", "flag", "nation", "country", "banner"],
- "🇹🇩": ["td", "flag", "nation", "country", "banner"],
- "🇨🇱": ["flag", "nation", "country", "banner"],
- "🇨🇳": ["china", "chinese", "prc", "flag", "country", "nation", "banner"],
- "🇨🇽": ["christmas", "island", "flag", "nation", "country", "banner"],
- "🇨🇨": ["cocos", "keeling", "islands", "flag", "nation", "country", "banner"],
- "🇨🇴": ["co", "flag", "nation", "country", "banner"],
- "🇰🇲": ["km", "flag", "nation", "country", "banner"],
- "🇨🇬": ["congo", "flag", "nation", "country", "banner"],
- "🇨🇩": ["congo", "democratic", "republic", "flag", "nation", "country", "banner"],
- "🇨🇰": ["cook", "islands", "flag", "nation", "country", "banner"],
- "🇨🇷": ["costa", "rica", "flag", "nation", "country", "banner"],
- "🇭🇷": ["hr", "flag", "nation", "country", "banner"],
- "🇨🇺": ["cu", "flag", "nation", "country", "banner"],
- "🇨🇼": ["curaçao", "flag", "nation", "country", "banner"],
- "🇨🇾": ["cy", "flag", "nation", "country", "banner"],
- "🇨🇿": ["cz", "flag", "nation", "country", "banner"],
- "🇩🇰": ["dk", "flag", "nation", "country", "banner"],
- "🇩🇯": ["dj", "flag", "nation", "country", "banner"],
- "🇩🇲": ["dm", "flag", "nation", "country", "banner"],
- "🇩🇴": ["dominican", "republic", "flag", "nation", "country", "banner"],
- "🇪🇨": ["ec", "flag", "nation", "country", "banner"],
- "🇪🇬": ["eg", "flag", "nation", "country", "banner"],
- "🇸🇻": ["el", "salvador", "flag", "nation", "country", "banner"],
- "🇬🇶": ["equatorial", "gn", "flag", "nation", "country", "banner"],
- "🇪🇷": ["er", "flag", "nation", "country", "banner"],
- "🇪🇪": ["ee", "flag", "nation", "country", "banner"],
- "🇪🇹": ["et", "flag", "nation", "country", "banner"],
- "🇪🇺": ["european", "union", "flag", "banner"],
- "🇫🇰": ["falkland", "islands", "malvinas", "flag", "nation", "country", "banner"],
- "🇫🇴": ["faroe", "islands", "flag", "nation", "country", "banner"],
- "🇫🇯": ["fj", "flag", "nation", "country", "banner"],
- "🇫🇮": ["fi", "flag", "nation", "country", "banner"],
- "🇫🇷": ["banner", "flag", "nation", "france", "french", "country"],
- "🇬🇫": ["french", "guiana", "flag", "nation", "country", "banner"],
- "🇵🇫": ["french", "polynesia", "flag", "nation", "country", "banner"],
- "🇹🇫": ["french", "southern", "territories", "flag", "nation", "country", "banner"],
- "🇬🇦": ["ga", "flag", "nation", "country", "banner"],
- "🇬🇲": ["gm", "flag", "nation", "country", "banner"],
- "🇬🇪": ["ge", "flag", "nation", "country", "banner"],
- "🇩🇪": ["german", "nation", "flag", "country", "banner"],
- "🇬🇭": ["gh", "flag", "nation", "country", "banner"],
- "🇬🇮": ["gi", "flag", "nation", "country", "banner"],
- "🇬🇷": ["gr", "flag", "nation", "country", "banner"],
- "🇬🇱": ["gl", "flag", "nation", "country", "banner"],
- "🇬🇩": ["gd", "flag", "nation", "country", "banner"],
- "🇬🇵": ["gp", "flag", "nation", "country", "banner"],
- "🇬🇺": ["gu", "flag", "nation", "country", "banner"],
- "🇬🇹": ["gt", "flag", "nation", "country", "banner"],
- "🇬🇬": ["gg", "flag", "nation", "country", "banner"],
- "🇬🇳": ["gn", "flag", "nation", "country", "banner"],
- "🇬🇼": ["gw", "bissau", "flag", "nation", "country", "banner"],
- "🇬🇾": ["gy", "flag", "nation", "country", "banner"],
- "🇭🇹": ["ht", "flag", "nation", "country", "banner"],
- "🇭🇳": ["hn", "flag", "nation", "country", "banner"],
- "🇭🇰": ["hong", "kong", "flag", "nation", "country", "banner"],
- "🇭🇺": ["hu", "flag", "nation", "country", "banner"],
- "🇮🇸": ["is", "flag", "nation", "country", "banner"],
- "🇮🇳": ["in", "flag", "nation", "country", "banner"],
- "🇮🇩": ["flag", "nation", "country", "banner"],
- "🇮🇷": ["iran, ", "islamic", "republic", "flag", "nation", "country", "banner"],
- "🇮🇶": ["iq", "flag", "nation", "country", "banner"],
- "🇮🇪": ["ie", "flag", "nation", "country", "banner"],
- "🇮🇲": ["isle", "man", "flag", "nation", "country", "banner"],
- "🇮🇱": ["il", "flag", "nation", "country", "banner"],
- "🇮🇹": ["italy", "flag", "nation", "country", "banner"],
- "🇨🇮": ["ivory", "coast", "flag", "nation", "country", "banner"],
- "🇯🇲": ["jm", "flag", "nation", "country", "banner"],
- "🇯🇵": ["japanese", "nation", "flag", "country", "banner"],
- "🇯🇪": ["je", "flag", "nation", "country", "banner"],
- "🇯🇴": ["jo", "flag", "nation", "country", "banner"],
- "🇰🇿": ["kz", "flag", "nation", "country", "banner"],
- "🇰🇪": ["ke", "flag", "nation", "country", "banner"],
- "🇰🇮": ["ki", "flag", "nation", "country", "banner"],
- "🇽🇰": ["xk", "flag", "nation", "country", "banner"],
- "🇰🇼": ["kw", "flag", "nation", "country", "banner"],
- "🇰🇬": ["kg", "flag", "nation", "country", "banner"],
- "🇱🇦": ["lao", "democratic", "republic", "flag", "nation", "country", "banner"],
- "🇱🇻": ["lv", "flag", "nation", "country", "banner"],
- "🇱🇧": ["lb", "flag", "nation", "country", "banner"],
- "🇱🇸": ["ls", "flag", "nation", "country", "banner"],
- "🇱🇷": ["lr", "flag", "nation", "country", "banner"],
- "🇱🇾": ["ly", "flag", "nation", "country", "banner"],
- "🇱🇮": ["li", "flag", "nation", "country", "banner"],
- "🇱🇹": ["lt", "flag", "nation", "country", "banner"],
- "🇱🇺": ["lu", "flag", "nation", "country", "banner"],
- "🇲🇴": ["macao", "flag", "nation", "country", "banner"],
- "🇲🇰": ["macedonia, ", "flag", "nation", "country", "banner"],
- "🇲🇬": ["mg", "flag", "nation", "country", "banner"],
- "🇲🇼": ["mw", "flag", "nation", "country", "banner"],
- "🇲🇾": ["my", "flag", "nation", "country", "banner"],
- "🇲🇻": ["mv", "flag", "nation", "country", "banner"],
- "🇲🇱": ["ml", "flag", "nation", "country", "banner"],
- "🇲🇹": ["mt", "flag", "nation", "country", "banner"],
- "🇲🇭": ["marshall", "islands", "flag", "nation", "country", "banner"],
- "🇲🇶": ["mq", "flag", "nation", "country", "banner"],
- "🇲🇷": ["mr", "flag", "nation", "country", "banner"],
- "🇲🇺": ["mu", "flag", "nation", "country", "banner"],
- "🇾🇹": ["yt", "flag", "nation", "country", "banner"],
- "🇲🇽": ["mx", "flag", "nation", "country", "banner"],
- "🇫🇲": ["micronesia, ", "federated", "states", "flag", "nation", "country", "banner"],
- "🇲🇩": ["moldova, ", "republic", "flag", "nation", "country", "banner"],
- "🇲🇨": ["mc", "flag", "nation", "country", "banner"],
- "🇲🇳": ["mn", "flag", "nation", "country", "banner"],
- "🇲🇪": ["me", "flag", "nation", "country", "banner"],
- "🇲🇸": ["ms", "flag", "nation", "country", "banner"],
- "🇲🇦": ["ma", "flag", "nation", "country", "banner"],
- "🇲🇿": ["mz", "flag", "nation", "country", "banner"],
- "🇲🇲": ["mm", "flag", "nation", "country", "banner"],
- "🇳🇦": ["na", "flag", "nation", "country", "banner"],
- "🇳🇷": ["nr", "flag", "nation", "country", "banner"],
- "🇳🇵": ["np", "flag", "nation", "country", "banner"],
- "🇳🇱": ["nl", "flag", "nation", "country", "banner"],
- "🇳🇨": ["new", "caledonia", "flag", "nation", "country", "banner"],
- "🇳🇿": ["new", "zealand", "flag", "nation", "country", "banner"],
- "🇳🇮": ["ni", "flag", "nation", "country", "banner"],
- "🇳🇪": ["ne", "flag", "nation", "country", "banner"],
- "🇳🇬": ["flag", "nation", "country", "banner"],
- "🇳🇺": ["nu", "flag", "nation", "country", "banner"],
- "🇳🇫": ["norfolk", "island", "flag", "nation", "country", "banner"],
- "🇲🇵": ["northern", "mariana", "islands", "flag", "nation", "country", "banner"],
- "🇰🇵": ["north", "korea", "nation", "flag", "country", "banner"],
- "🇳🇴": ["no", "flag", "nation", "country", "banner"],
- "🇴🇲": ["om_symbol", "flag", "nation", "country", "banner"],
- "🇵🇰": ["pk", "flag", "nation", "country", "banner"],
- "🇵🇼": ["pw", "flag", "nation", "country", "banner"],
- "🇵🇸": ["palestine", "palestinian", "territories", "flag", "nation", "country", "banner"],
- "🇵🇦": ["pa", "flag", "nation", "country", "banner"],
- "🇵🇬": ["papua", "new", "guinea", "flag", "nation", "country", "banner"],
- "🇵🇾": ["py", "flag", "nation", "country", "banner"],
- "🇵🇪": ["pe", "flag", "nation", "country", "banner"],
- "🇵🇭": ["ph", "flag", "nation", "country", "banner"],
- "🇵🇳": ["pitcairn", "flag", "nation", "country", "banner"],
- "🇵🇱": ["pl", "flag", "nation", "country", "banner"],
- "🇵🇹": ["pt", "flag", "nation", "country", "banner"],
- "🇵🇷": ["puerto", "rico", "flag", "nation", "country", "banner"],
- "🇶🇦": ["qa", "flag", "nation", "country", "banner"],
- "🇷🇪": ["réunion", "flag", "nation", "country", "banner"],
- "🇷🇴": ["ro", "flag", "nation", "country", "banner"],
- "🇷🇺": ["russian", "federation", "flag", "nation", "country", "banner"],
- "🇷🇼": ["rw", "flag", "nation", "country", "banner"],
- "🇧🇱": ["saint", "barthélemy", "flag", "nation", "country", "banner"],
- "🇸🇭": ["saint", "helena", "ascension", "tristan", "cunha", "flag", "nation", "country", "banner"],
- "🇰🇳": ["saint", "kitts", "nevis", "flag", "nation", "country", "banner"],
- "🇱🇨": ["saint", "lucia", "flag", "nation", "country", "banner"],
- "🇵🇲": ["saint", "pierre", "miquelon", "flag", "nation", "country", "banner"],
- "🇻🇨": ["saint", "vincent", "grenadines", "flag", "nation", "country", "banner"],
- "🇼🇸": ["ws", "flag", "nation", "country", "banner"],
- "🇸🇲": ["san", "marino", "flag", "nation", "country", "banner"],
- "🇸🇹": ["sao", "tome", "principe", "flag", "nation", "country", "banner"],
- "🇸🇦": ["flag", "nation", "country", "banner"],
- "🇸🇳": ["sn", "flag", "nation", "country", "banner"],
- "🇷🇸": ["rs", "flag", "nation", "country", "banner"],
- "🇸🇨": ["sc", "flag", "nation", "country", "banner"],
- "🇸🇱": ["sierra", "leone", "flag", "nation", "country", "banner"],
- "🇸🇬": ["sg", "flag", "nation", "country", "banner"],
- "🇸🇽": ["sint", "maarten", "dutch", "flag", "nation", "country", "banner"],
- "🇸🇰": ["sk", "flag", "nation", "country", "banner"],
- "🇸🇮": ["si", "flag", "nation", "country", "banner"],
- "🇸🇧": ["solomon", "islands", "flag", "nation", "country", "banner"],
- "🇸🇴": ["so", "flag", "nation", "country", "banner"],
- "🇿🇦": ["south", "africa", "flag", "nation", "country", "banner"],
- "🇬🇸": ["south", "georgia", "sandwich", "islands", "flag", "nation", "country", "banner"],
- "🇰🇷": ["south", "korea", "nation", "flag", "country", "banner"],
- "🇸🇸": ["south", "sd", "flag", "nation", "country", "banner"],
- "🇪🇸": ["spain", "flag", "nation", "country", "banner"],
- "🇱🇰": ["sri", "lanka", "flag", "nation", "country", "banner"],
- "🇸🇩": ["sd", "flag", "nation", "country", "banner"],
- "🇸🇷": ["sr", "flag", "nation", "country", "banner"],
- "🇸🇿": ["sz", "flag", "nation", "country", "banner"],
- "🇸🇪": ["se", "flag", "nation", "country", "banner"],
- "🇨🇭": ["ch", "flag", "nation", "country", "banner"],
- "🇸🇾": ["syrian", "arab", "republic", "flag", "nation", "country", "banner"],
- "🇹🇼": ["tw", "flag", "nation", "country", "banner"],
- "🇹🇯": ["tj", "flag", "nation", "country", "banner"],
- "🇹🇿": ["tanzania, ", "united", "republic", "flag", "nation", "country", "banner"],
- "🇹🇭": ["th", "flag", "nation", "country", "banner"],
- "🇹🇱": ["timor", "leste", "flag", "nation", "country", "banner"],
- "🇹🇬": ["tg", "flag", "nation", "country", "banner"],
- "🇹🇰": ["tk", "flag", "nation", "country", "banner"],
- "🇹🇴": ["to", "flag", "nation", "country", "banner"],
- "🇹🇹": ["trinidad", "tobago", "flag", "nation", "country", "banner"],
- "🇹🇦": ["flag", "nation", "country", "banner"],
- "🇹🇳": ["tn", "flag", "nation", "country", "banner"],
- "🇹🇷": ["turkey", "flag", "nation", "country", "banner"],
- "🇹🇲": ["flag", "nation", "country", "banner"],
- "🇹🇨": ["turks", "caicos", "islands", "flag", "nation", "country", "banner"],
- "🇹🇻": ["flag", "nation", "country", "banner"],
- "🇺🇬": ["ug", "flag", "nation", "country", "banner"],
- "🇺🇦": ["ua", "flag", "nation", "country", "banner"],
- "🇦🇪": ["united", "arab", "emirates", "flag", "nation", "country", "banner"],
- "🇬🇧": ["united", "kingdom", "great", "britain", "northern", "ireland", "flag", "nation", "country", "banner", "british", "UK", "english", "england", "union jack"],
+ "🇦🇴": ["ao", "angola", "flag", "nation", "country", "banner"],
+ "🇦🇮": ["ai", "anguilla", "flag", "nation", "country", "banner"],
+ "🇦🇶": ["aq", "antarctique", "flag", "nation", "country", "banner"],
+ "🇦🇬": ["ag", "antigua", "barbuda", "flag", "nation", "country", "banner"],
+ "🇦🇷": ["ar", "argentina", "flag", "nation", "country", "banner"],
+ "🇦🇲": ["am", "armenia", "flag", "nation", "country", "banner"],
+ "🇦🇼": ["aw", "aruba", "flag", "nation", "country", "banner"],
+ "🇦🇨": ["ac", "ascension", "island", "flag", "nation", "country", "banner"],
+ "🇦🇺": ["au", "australia", "flag", "nation", "country", "banner"],
+ "🇦🇹": ["at", "austria", "flag", "nation", "country", "banner"],
+ "🇦🇿": ["az", "azerbaijan", "flag", "nation", "country", "banner"],
+ "🇧🇸": ["bs", "bahamas", "flag", "nation", "country", "banner"],
+ "🇧🇭": ["bh", "bahrain", "flag", "nation", "country", "banner"],
+ "🇧🇩": ["bd", "bangladesh", "flag", "nation", "country", "banner"],
+ "🇧🇧": ["bb", "barbados", "flag", "nation", "country", "banner"],
+ "🇧🇾": ["by", "belarus", "flag", "nation", "country", "banner"],
+ "🇧🇪": ["be", "belgium", "flag", "nation", "country", "banner"],
+ "🇧🇿": ["bz", "belize", "flag", "nation", "country", "banner"],
+ "🇧🇯": ["bj", "benin", "flag", "nation", "country", "banner"],
+ "🇧🇲": ["bm", "bermuda", "flag", "nation", "country", "banner"],
+ "🇧🇹": ["bt", "bhutan", "flag", "nation", "country", "banner"],
+ "🇧🇴": ["bo", "bolivia", "flag", "nation", "country", "banner"],
+ "🇧🇶": ["bq", "bonaire", "flag", "nation", "country", "banner"],
+ "🇧🇦": ["ba", "bosnia", "herzegovina", "flag", "nation", "country", "banner"],
+ "🇧🇼": ["bw", "botswana", "flag", "nation", "country", "banner"],
+ "🇧🇷": ["br", "brazil", "flag", "nation", "country", "banner"],
+ "🇮🇴": ["io", "british", "indian", "ocean", "territory", "flag", "nation", "country", "banner"],
+ "🇻🇬": ["vg", "british", "virgin", "islands", "bvi", "flag", "nation", "country", "banner"],
+ "🇧🇳": ["bn", "brunei", "darussalam", "flag", "nation", "country", "banner"],
+ "🇧🇬": ["bg", "bulgaria", "flag", "nation", "country", "banner"],
+ "🇧🇫": ["bf", "burkina", "faso", "flag", "nation", "country", "banner"],
+ "🇧🇮": ["bi", "burundi", "flag", "nation", "country", "banner"],
+ "🇨🇻": ["cv", "cabo", "verde", "flag", "nation", "country", "banner"],
+ "🇰🇭": ["kh", "cambodia", "flag", "nation", "country", "banner"],
+ "🇨🇲": ["cm", "cameroon", "flag", "nation", "country", "banner"],
+ "🇨🇦": ["ca", "canada", "flag", "nation", "country", "banner"],
+ "🇮🇨": ["ic", "canary", "islands", "flag", "nation", "country", "banner"],
+ "🇰🇾": ["ky", "cayman", "islands", "flag", "nation", "country", "banner"],
+ "🇨🇫": ["cf", "central", "african", "republic", "flag", "nation", "country", "banner"],
+ "🇹🇩": ["td", "chad", "flag", "nation", "country", "banner"],
+ "🇨🇱": ["cl", "chile", "flag", "nation", "country", "banner"],
+ "🇨🇳": ["cn", "china", "chinese", "prc", "flag", "country", "nation", "banner"],
+ "🇨🇽": ["cx", "christmas", "island", "flag", "nation", "country", "banner"],
+ "🇨🇨": ["cc", "cocos", "keeling", "islands", "flag", "nation", "country", "banner"],
+ "🇨🇴": ["co", "colombia", "flag", "nation", "country", "banner"],
+ "🇰🇲": ["km", "comoros", "flag", "nation", "country", "banner"],
+ "🇨🇬": ["cg", "republic", "congo", "flag", "nation", "country", "banner"],
+ "🇨🇩": ["cd", "democratic", "republic", "congo", "flag", "nation", "country", "banner"],
+ "🇨🇰": ["ck", "cook", "islands", "flag", "nation", "country", "banner"],
+ "🇨🇷": ["cr", "costa", "rica", "flag", "nation", "country", "banner"],
+ "🇭🇷": ["hr", "croatia", "flag", "nation", "country", "banner"],
+ "🇨🇺": ["cu", "cuba", "flag", "nation", "country", "banner"],
+ "🇨🇼": ["cw", "curacao", "curaçao", "flag", "nation", "country", "banner"],
+ "🇨🇾": ["cy", "cyprus", "flag", "nation", "country", "banner"],
+ "🇨🇿": ["cz", "czech", "republic", "flag", "nation", "country", "banner"],
+ "🇩🇰": ["dk", "denmark", "flag", "nation", "country", "banner"],
+ "🇩🇯": ["dj", "djibouti", "flag", "nation", "country", "banner"],
+ "🇩🇲": ["dm", "dominica", "flag", "nation", "country", "banner"],
+ "🇩🇴": ["do", "dominican", "republic", "flag", "nation", "country", "banner"],
+ "🇪🇨": ["ec", "ecuador", "flag", "nation", "country", "banner"],
+ "🇪🇬": ["eg", "egypt", "flag", "nation", "country", "banner"],
+ "🇸🇻": ["sv", "el", "salvador", "flag", "nation", "country", "banner"],
+ "🇬🇶": ["gq", "equatorial", "guinea", "flag", "nation", "country", "banner"],
+ "🇪🇷": ["er", "eritrea", "flag", "nation", "country", "banner"],
+ "🇪🇪": ["ee", "estonia", "flag", "nation", "country", "banner"],
+ "🇪🇹": ["et", "ethiopia", "flag", "nation", "country", "banner"],
+ "🇪🇺": ["eu", "european", "union", "flag", "banner"],
+ "🇫🇰": ["fk", "falkland", "islands", "malvinas", "flag", "nation", "country", "banner"],
+ "🇫🇴": ["fo", "faroe", "islands", "flag", "nation", "country", "banner"],
+ "🇫🇯": ["fj", "fiji", "flag", "nation", "country", "banner"],
+ "🇫🇮": ["fi", "finland", "flag", "nation", "country", "banner"],
+ "🇫🇷": ["fr", "banner", "flag", "nation", "france", "french", "country"],
+ "🇬🇫": ["gf", "french", "guiana", "flag", "nation", "country", "banner"],
+ "🇵🇫": ["pf", "french", "polynesia", "flag", "nation", "country", "banner"],
+ "🇹🇫": ["tf", "french", "southern", "territories", "flag", "nation", "country", "banner"],
+ "🇬🇦": ["ga", "gabon", "flag", "nation", "country", "banner"],
+ "🇬🇲": ["gm", "gambia", "flag", "nation", "country", "banner"],
+ "🇬🇪": ["ge", "georgia", "flag", "nation", "country", "banner"],
+ "🇩🇪": ["de", "deutschland", "german", "nation", "flag", "country", "banner"],
+ "🇬🇭": ["gh", "ghana", "flag", "nation", "country", "banner"],
+ "🇬🇮": ["gi", "gibraltar", "flag", "nation", "country", "banner"],
+ "🇬🇷": ["gr", "greece", "flag", "nation", "country", "banner"],
+ "🇬🇱": ["gl", "green", "land", "flag", "nation", "country", "banner"],
+ "🇬🇩": ["gd", "grenada", "flag", "nation", "country", "banner"],
+ "🇬🇵": ["gp", "guadeloupe", "flag", "nation", "country", "banner"],
+ "🇬🇺": ["gu", "guam", "flag", "nation", "country", "banner"],
+ "🇬🇹": ["gt", "guatemala", "flag", "nation", "country", "banner"],
+ "🇬🇬": ["gg", "guernsey", "flag", "nation", "country", "banner"],
+ "🇬🇳": ["gn", "guinea", "flag", "nation", "country", "banner"],
+ "🇬🇼": ["gw", "guiana", "bissau", "flag", "nation", "country", "banner"],
+ "🇬🇾": ["gy", "guyana", "flag", "nation", "country", "banner"],
+ "🇭🇹": ["ht", "haiti", "flag", "nation", "country", "banner"],
+ "🇭🇳": ["hn", "honduras", "flag", "nation", "country", "banner"],
+ "🇭🇰": ["hk", "hong", "kong", "flag", "nation", "country", "banner"],
+ "🇭🇺": ["hu", "hungary", "flag", "nation", "country", "banner"],
+ "🇮🇸": ["is", "iceland", "Ísland", "flag", "nation", "country", "banner"],
+ "🇮🇳": ["in", "india", "flag", "nation", "country", "banner"],
+ "🇮🇩": ["id", "indonesia", "flag", "nation", "country", "banner"],
+ "🇮🇷": ["ir", "iran", "islamic", "republic", "flag", "nation", "country", "banner"],
+ "🇮🇶": ["iq", "iraq", "flag", "nation", "country", "banner"],
+ "🇮🇪": ["ie", "ireland", "flag", "nation", "country", "banner"],
+ "🇮🇲": ["im", "isle", "man", "flag", "nation", "country", "banner"],
+ "🇮🇱": ["il", "israel", "flag", "nation", "country", "banner"],
+ "🇮🇹": ["it", "italy", "flag", "nation", "country", "banner"],
+ "🇨🇮": ["ci", "cote", "divoire", "Côte", "d'Ivoire", "ivory", "coast", "flag", "nation", "country", "banner"],
+ "🇯🇲": ["jm", "jamaica", "flag", "nation", "country", "banner"],
+ "🇯🇵": ["jp", "japan", "japanese", "nation", "flag", "country", "banner"],
+ "🇯🇪": ["je", "jersey", "flag", "nation", "country", "banner"],
+ "🇯🇴": ["jo", "jordan", "flag", "nation", "country", "banner"],
+ "🇰🇿": ["kz", "kazakhstan", "flag", "nation", "country", "banner"],
+ "🇰🇪": ["ke", "kenya", "flag", "nation", "country", "banner"],
+ "🇰🇮": ["ki", "kiribati", "flag", "nation", "country", "banner"],
+ "🇽🇰": ["xk", "kosovo", "flag", "nation", "country", "banner"],
+ "🇰🇼": ["kw", "kuwait", "flag", "nation", "country", "banner"],
+ "🇰🇬": ["kg", "kyrgyzstan", "kyrgyz", "flag", "nation", "country", "banner"],
+ "🇱🇦": ["la", "laos", "lao", "democratic", "republic", "flag", "nation", "country", "banner"],
+ "🇱🇻": ["lv", "latvia", "flag", "nation", "country", "banner"],
+ "🇱🇧": ["lb", "lebanon", "flag", "nation", "country", "banner"],
+ "🇱🇸": ["ls", "lesotho", "flag", "nation", "country", "banner"],
+ "🇱🇷": ["lr", "liberia", "flag", "nation", "country", "banner"],
+ "🇱🇾": ["ly", "libya", "flag", "nation", "country", "banner"],
+ "🇱🇮": ["li", "liechtenstein", "flag", "nation", "country", "banner"],
+ "🇱🇹": ["lt", "lithuania", "flag", "nation", "country", "banner"],
+ "🇱🇺": ["lu", "luxembourg", "flag", "nation", "country", "banner"],
+ "🇲🇴": ["mo", "macao", "macau", "flag", "nation", "country", "banner"],
+ "🇲🇰": ["mk", "north", "macedonia", "flag", "nation", "country", "banner"],
+ "🇲🇬": ["mg", "madagascar", "flag", "nation", "country", "banner"],
+ "🇲🇼": ["mw", "malawi", "flag", "nation", "country", "banner"],
+ "🇲🇾": ["my", "malaysia", "flag", "nation", "country", "banner"],
+ "🇲🇻": ["mv", "maldives", "republic", "flag", "nation", "country", "banner"],
+ "🇲🇱": ["ml", "mali", "flag", "nation", "country", "banner"],
+ "🇲🇹": ["mt", "malta", "flag", "nation", "country", "banner"],
+ "🇲🇭": ["mh", "marshall", "islands", "flag", "nation", "country", "banner"],
+ "🇲🇶": ["mq", "martinique", "flag", "nation", "country", "banner"],
+ "🇲🇷": ["mr", "mauritania", "flag", "nation", "country", "banner"],
+ "🇲🇺": ["mu", "mauritius", "flag", "nation", "country", "banner"],
+ "🇾🇹": ["yt", "mayotte", "flag", "nation", "country", "banner"],
+ "🇲🇽": ["mx", "mexico", "flag", "nation", "country", "banner"],
+ "🇫🇲": ["fm", "micronesia", "federated", "states", "flag", "nation", "country", "banner"],
+ "🇲🇩": ["md", "moldova", "republic", "flag", "nation", "country", "banner"],
+ "🇲🇨": ["mc", "monaco", "flag", "nation", "country", "banner"],
+ "🇲🇳": ["mn", "mongolia", "flag", "nation", "country", "banner"],
+ "🇲🇪": ["me", "montenegro", "flag", "nation", "country", "banner"],
+ "🇲🇸": ["ms", "montserrat", "flag", "nation", "country", "banner"],
+ "🇲🇦": ["ma", "morocco", "flag", "nation", "country", "banner"],
+ "🇲🇿": ["mz", "mozambique", "flag", "nation", "country", "banner"],
+ "🇲🇲": ["mm", "myanmar", "flag", "nation", "country", "banner"],
+ "🇳🇦": ["na", "namibia", "flag", "nation", "country", "banner"],
+ "🇳🇷": ["nr", "nauru", "flag", "nation", "country", "banner"],
+ "🇳🇵": ["np", "nepal", "flag", "nation", "country", "banner"],
+ "🇳🇱": ["nl", "netherlands", "flag", "nation", "country", "banner"],
+ "🇳🇨": ["nc", "new", "caledonia", "flag", "nation", "country", "banner"],
+ "🇳🇿": ["nz", "new", "zealand", "flag", "nation", "country", "banner"],
+ "🇳🇮": ["ni", "nicaragua", "flag", "nation", "country", "banner"],
+ "🇳🇪": ["ne", "niger", "flag", "nation", "country", "banner"],
+ "🇳🇬": ["ng", "nigeria", "flag", "nation", "country", "banner"],
+ "🇳🇺": ["nu", "niue", "flag", "nation", "country", "banner"],
+ "🇳🇫": ["nf", "norfolk", "island", "flag", "nation", "country", "banner"],
+ "🇲🇵": ["mp", "northern", "mariana", "islands", "flag", "nation", "country", "banner"],
+ "🇰🇵": ["kp", "democratic", "people", "republic", "north", "korea", "nation", "flag", "country", "banner"],
+ "🇳🇴": ["no", "norway", "flag", "nation", "country", "banner"],
+ "🇴🇲": ["om", "oman", "flag", "nation", "country", "banner"],
+ "🇵🇰": ["pk", "pakistan", "flag", "nation", "country", "banner"],
+ "🇵🇼": ["pw", "palau", "flag", "nation", "country", "banner"],
+ "🇵🇸": ["ps", "palestine", "palestinian", "territories", "flag", "nation", "country", "banner"],
+ "🇵🇦": ["pa", "panama", "flag", "nation", "country", "banner"],
+ "🇵🇬": ["pg", "papua", "new", "guinea", "flag", "nation", "country", "banner"],
+ "🇵🇾": ["py", "paraguay", "flag", "nation", "country", "banner"],
+ "🇵🇪": ["pe", "peru", "flag", "nation", "country", "banner"],
+ "🇵🇭": ["ph", "philippines", "flag", "nation", "country", "banner"],
+ "🇵🇳": ["pn", "pitcairn", "flag", "nation", "country", "banner"],
+ "🇵🇱": ["pl", "poland", "flag", "nation", "country", "banner"],
+ "🇵🇹": ["pt", "portugal", "flag", "nation", "country", "banner"],
+ "🇵🇷": ["pr", "puerto", "rico", "flag", "nation", "country", "banner"],
+ "🇶🇦": ["qa", "qatar", "flag", "nation", "country", "banner"],
+ "🇷🇪": ["re", "reunion", "réunion", "flag", "nation", "country", "banner"],
+ "🇷🇴": ["ro", "romania", "flag", "nation", "country", "banner"],
+ "🇷🇺": ["ru", "russian", "federation", "flag", "nation", "country", "banner"],
+ "🇷🇼": ["rw", "rwanda", "flag", "nation", "country", "banner"],
+ "🇧🇱": ["bl", "saint", "barthélemy", "flag", "nation", "country", "banner"],
+ "🇸🇭": ["sh", "saint", "helena", "ascension", "tristan", "cunha", "flag", "nation", "country", "banner"],
+ "🇰🇳": ["kn", "saint", "kitts", "nevis", "flag", "nation", "country", "banner"],
+ "🇱🇨": ["lc", "saint", "lucia", "flag", "nation", "country", "banner"],
+ "🇵🇲": ["pm", "saint", "pierre", "miquelon", "flag", "nation", "country", "banner"],
+ "🇻🇨": ["vc", "saint", "vincent", "grenadines", "flag", "nation", "country", "banner"],
+ "🇼🇸": ["ws", "western", "samoa", "flag", "nation", "country", "banner"],
+ "🇸🇲": ["sm", "san", "marino", "flag", "nation", "country", "banner"],
+ "🇸🇹": ["st", "sao", "tome", "principe", "flag", "nation", "country", "banner"],
+ "🇸🇦": ["saudi", "arabia", "flag", "nation", "country", "banner"],
+ "🇸🇳": ["sn", "senegal", "flag", "nation", "country", "banner"],
+ "🇷🇸": ["rs", "serbia", "flag", "nation", "country", "banner"],
+ "🇸🇨": ["sc", "seychelles", "flag", "nation", "country", "banner"],
+ "🇸🇱": ["sl", "sierra", "leone", "flag", "nation", "country", "banner"],
+ "🇸🇬": ["sg", "singapore", "flag", "nation", "country", "banner"],
+ "🇸🇽": ["sx", "sint", "maarten", "dutch", "flag", "nation", "country", "banner"],
+ "🇸🇰": ["sk", "slovakia", "flag", "nation", "country", "banner"],
+ "🇸🇮": ["si", "slovenia", "flag", "nation", "country", "banner"],
+ "🇸🇧": ["sb", "solomon", "islands", "flag", "nation", "country", "banner"],
+ "🇸🇴": ["so", "somalia", "flag", "nation", "country", "banner"],
+ "🇿🇦": ["za", "south", "africa", "flag", "nation", "country", "banner"],
+ "🇬🇸": ["gs", "south", "georgia", "sandwich", "islands", "flag", "nation", "country", "banner"],
+ "🇰🇷": ["kr", "south", "korea", "nation", "flag", "country", "banner"],
+ "🇸🇸": ["ss", "south", "sudan", "flag", "nation", "country", "banner"],
+ "🇪🇸": ["es", "spain", "españa", "flag", "nation", "country", "banner"],
+ "🇱🇰": ["lk", "sri", "lanka", "flag", "nation", "country", "banner"],
+ "🇸🇩": ["sd", "sudan", "flag", "nation", "country", "banner"],
+ "🇸🇷": ["sr", "suriname", "flag", "nation", "country", "banner"],
+ "🇸🇿": ["sz", "eswatini", "flag", "nation", "country", "banner"],
+ "🇸🇪": ["se", "sweden", "flag", "nation", "country", "banner"],
+ "🇨🇭": ["ch", "switzerland", "confoederatio", "helvetica", "flag", "nation", "country", "banner"],
+ "🇸🇾": ["sy", "syrian", "arab", "republic", "flag", "nation", "country", "banner"],
+ "🇹🇼": ["tw", "taiwan", "flag", "nation", "country", "banner"],
+ "🇹🇯": ["tj", "tajikistan", "flag", "nation", "country", "banner"],
+ "🇹🇿": ["tz", "tanzania", "united", "republic", "flag", "nation", "country", "banner"],
+ "🇹🇭": ["th", "thailand", "flag", "nation", "country", "banner"],
+ "🇹🇱": ["tl", "timor", "leste", "flag", "nation", "country", "banner"],
+ "🇹🇬": ["tg", "togo", "flag", "nation", "country", "banner"],
+ "🇹🇰": ["tk", "tokelau", "flag", "nation", "country", "banner"],
+ "🇹🇴": ["to", "tonga", "flag", "nation", "country", "banner"],
+ "🇹🇹": ["tt", "trinidad", "tobago", "flag", "nation", "country", "banner"],
+ "🇹🇦": ["ta", "tristan", "da", "cunha", "flag", "nation", "country", "banner"],
+ "🇹🇳": ["tn", "tunisia", "flag", "nation", "country", "banner"],
+ "🇹🇷": ["tr", "turkey", "türkiye", "flag", "nation", "country", "banner"],
+ "🇹🇲": ["tm", "turkmenistan", "flag", "nation", "country", "banner"],
+ "🇹🇨": ["tc", "turks", "caicos", "islands", "flag", "nation", "country", "banner"],
+ "🇹🇻": ["tv", "tuvalu", "flag", "nation", "country", "banner"],
+ "🇺🇬": ["ug", "uganda", "flag", "nation", "country", "banner"],
+ "🇺🇦": ["ua", "ukraine", "flag", "nation", "country", "banner"],
+ "🇦🇪": ["ae", "united", "arab", "emirates", "flag", "nation", "country", "banner"],
+ "🇬🇧": ["gb", "united", "kingdom", "great", "britain", "northern", "ireland", "flag", "nation", "country", "banner", "british", "uk", "english", "england", "union jack"],
"🏴󠁧󠁢󠁥󠁮󠁧󠁿": ["flag", "english"],
"🏴󠁧󠁢󠁳󠁣󠁴󠁿": ["flag", "scottish"],
"🏴󠁧󠁢󠁷󠁬󠁳󠁿": ["flag", "welsh"],
- "🇺🇸": ["united", "states", "america", "flag", "nation", "country", "banner"],
- "🇻🇮": ["virgin", "islands", "us", "flag", "nation", "country", "banner"],
- "🇺🇾": ["uy", "flag", "nation", "country", "banner"],
- "🇺🇿": ["uz", "flag", "nation", "country", "banner"],
- "🇻🇺": ["vu", "flag", "nation", "country", "banner"],
- "🇻🇦": ["vatican", "city", "flag", "nation", "country", "banner"],
- "🇻🇪": ["ve", "bolivarian", "republic", "flag", "nation", "country", "banner"],
- "🇻🇳": ["viet", "nam", "flag", "nation", "country", "banner"],
- "🇼🇫": ["wallis", "futuna", "flag", "nation", "country", "banner"],
- "🇪🇭": ["western", "sahara", "flag", "nation", "country", "banner"],
- "🇾🇪": ["ye", "flag", "nation", "country", "banner"],
- "🇿🇲": ["zm", "flag", "nation", "country", "banner"],
- "🇿🇼": ["zw", "flag", "nation", "country", "banner"],
- "🇺🇳": ["un", "flag", "banner"],
+ "🇺🇸": ["us", "usa", "united", "states", "america", "flag", "nation", "country", "banner"],
+ "🇻🇮": ["vi", "virgin", "islands", "us", "flag", "nation", "country", "banner"],
+ "🇺🇾": ["uy", "uruguay", "flag", "nation", "country", "banner"],
+ "🇺🇿": ["uz", "uzbekistan", "flag", "nation", "country", "banner"],
+ "🇻🇺": ["vu", "vanuatu", "flag", "nation", "country", "banner"],
+ "🇻🇦": ["va", "vatican", "city", "flag", "nation", "country", "banner"],
+ "🇻🇪": ["ve", "venezuela", "flag", "nation", "country", "banner"],
+ "🇻🇳": ["vn", "viet", "nam", "flag", "nation", "country", "banner"],
+ "🇼🇫": ["wf", "wallis", "futuna", "flag", "nation", "country", "banner"],
+ "🇪🇭": ["eh", "western", "sahara", "flag", "nation", "country", "banner"],
+ "🇾🇪": ["ye", "yemen", "flag", "nation", "country", "banner"],
+ "🇿🇲": ["zm", "zambia", "flag", "nation", "country", "banner"],
+ "🇿🇼": ["zw", "zimbabwe", "flag", "nation", "country", "banner"],
+ "🇺🇳": ["un", "united", "nation", "flag", "banner"],
"🏴‍☠️": ["skull", "crossbones", "flag", "banner"]
}
diff --git a/packages/frontend/src/widgets/WidgetActivity.chart.vue b/packages/frontend/src/widgets/WidgetActivity.chart.vue
index 9cfd845ace..a207071324 100644
--- a/packages/frontend/src/widgets/WidgetActivity.chart.vue
+++ b/packages/frontend/src/widgets/WidgetActivity.chart.vue
@@ -34,18 +34,19 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
+import { ref } from 'vue';
const props = defineProps<{
activity: any[]
}>();
-let viewBoxX: number = $ref(147);
-let viewBoxY: number = $ref(60);
-let zoom: number = $ref(1);
-let pos: number = $ref(0);
-let pointsNote: any = $ref(null);
-let pointsReply: any = $ref(null);
-let pointsRenote: any = $ref(null);
-let pointsTotal: any = $ref(null);
+const viewBoxX = ref(147);
+const viewBoxY = ref(60);
+const zoom = ref(1);
+const pos = ref(0);
+const pointsNote = ref<any>(null);
+const pointsReply = ref<any>(null);
+const pointsRenote = ref<any>(null);
+const pointsTotal = ref<any>(null);
function dragListen(fn) {
window.addEventListener('mousemove', fn);
@@ -62,17 +63,17 @@ function dragClear(fn) {
function onMousedown(ev) {
const clickX = ev.clientX;
const clickY = ev.clientY;
- const baseZoom = zoom;
- const basePos = pos;
+ const baseZoom = zoom.value;
+ const basePos = pos.value;
// 動かした時
dragListen(me => {
let moveLeft = me.clientX - clickX;
let moveTop = me.clientY - clickY;
- zoom = Math.max(1, baseZoom + (-moveTop / 20));
- pos = Math.min(0, basePos + moveLeft);
- if (pos < -(((props.activity.length - 1) * zoom) - viewBoxX)) pos = -(((props.activity.length - 1) * zoom) - viewBoxX);
+ zoom.value = Math.max(1, baseZoom + (-moveTop / 20));
+ pos.value = Math.min(0, basePos + moveLeft);
+ if (pos.value < -(((props.activity.length - 1) * zoom.value) - viewBoxX.value)) pos.value = -(((props.activity.length - 1) * zoom.value) - viewBoxX.value);
render();
});
@@ -82,10 +83,10 @@ function render() {
const peak = Math.max(...props.activity.map(d => d.total));
if (peak !== 0) {
const activity = props.activity.slice().reverse();
- pointsNote = activity.map((d, i) => `${(i * zoom) + pos},${(1 - (d.notes / peak)) * viewBoxY}`).join(' ');
- pointsReply = activity.map((d, i) => `${(i * zoom) + pos},${(1 - (d.replies / peak)) * viewBoxY}`).join(' ');
- pointsRenote = activity.map((d, i) => `${(i * zoom) + pos},${(1 - (d.renotes / peak)) * viewBoxY}`).join(' ');
- pointsTotal = activity.map((d, i) => `${(i * zoom) + pos},${(1 - (d.total / peak)) * viewBoxY}`).join(' ');
+ pointsNote.value = activity.map((d, i) => `${(i * zoom.value) + pos.value},${(1 - (d.notes / peak)) * viewBoxY.value}`).join(' ');
+ pointsReply.value = activity.map((d, i) => `${(i * zoom.value) + pos.value},${(1 - (d.replies / peak)) * viewBoxY.value}`).join(' ');
+ pointsRenote.value = activity.map((d, i) => `${(i * zoom.value) + pos.value},${(1 - (d.renotes / peak)) * viewBoxY.value}`).join(' ');
+ pointsTotal.value = activity.map((d, i) => `${(i * zoom.value) + pos.value},${(1 - (d.total / peak)) * viewBoxY.value}`).join(' ');
}
}
</script>
diff --git a/packages/frontend/src/widgets/WidgetActivity.vue b/packages/frontend/src/widgets/WidgetActivity.vue
index 788a9f2c7b..b107a47d8c 100644
--- a/packages/frontend/src/widgets/WidgetActivity.vue
+++ b/packages/frontend/src/widgets/WidgetActivity.vue
@@ -21,7 +21,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { ref } from 'vue';
-import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js';
+import { useWidgetPropsManager, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js';
import XCalendar from './WidgetActivity.calendar.vue';
import XChart from './WidgetActivity.chart.vue';
import { GetFormResultType } from '@/scripts/form.js';
diff --git a/packages/frontend/src/widgets/WidgetAichan.vue b/packages/frontend/src/widgets/WidgetAichan.vue
index 76b35f6fed..fef026244c 100644
--- a/packages/frontend/src/widgets/WidgetAichan.vue
+++ b/packages/frontend/src/widgets/WidgetAichan.vue
@@ -11,7 +11,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { onMounted, onUnmounted, shallowRef } from 'vue';
-import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js';
+import { useWidgetPropsManager, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js';
import { GetFormResultType } from '@/scripts/form.js';
const name = 'ai';
@@ -72,5 +72,6 @@ defineExpose<WidgetComponentExpose>({
height: 350px;
border: none;
pointer-events: none;
+ color-scheme: light;
}
</style>
diff --git a/packages/frontend/src/widgets/WidgetAiscript.vue b/packages/frontend/src/widgets/WidgetAiscript.vue
index 8fb4ebe0f2..c17e9728a5 100644
--- a/packages/frontend/src/widgets/WidgetAiscript.vue
+++ b/packages/frontend/src/widgets/WidgetAiscript.vue
@@ -21,7 +21,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { ref } from 'vue';
import { Interpreter, Parser, utils } from '@syuilo/aiscript';
-import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js';
+import { useWidgetPropsManager, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js';
import { GetFormResultType } from '@/scripts/form.js';
import * as os from '@/os.js';
import MkContainer from '@/components/MkContainer.vue';
diff --git a/packages/frontend/src/widgets/WidgetAiscriptApp.vue b/packages/frontend/src/widgets/WidgetAiscriptApp.vue
index 53b6020ffc..10248a840a 100644
--- a/packages/frontend/src/widgets/WidgetAiscriptApp.vue
+++ b/packages/frontend/src/widgets/WidgetAiscriptApp.vue
@@ -15,7 +15,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { onMounted, Ref, ref, watch } from 'vue';
import { Interpreter, Parser } from '@syuilo/aiscript';
-import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js';
+import { useWidgetPropsManager, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js';
import { GetFormResultType } from '@/scripts/form.js';
import * as os from '@/os.js';
import { createAiScriptEnv } from '@/scripts/aiscript/api.js';
@@ -52,7 +52,7 @@ const { widgetProps, configure } = useWidgetPropsManager(name,
const parser = new Parser();
const root = ref<AsUiRoot>();
-const components: Ref<AsUiComponent>[] = $ref([]);
+const components = ref<Ref<AsUiComponent>[]>([]);
async function run() {
const aiscript = new Interpreter({
@@ -60,7 +60,7 @@ async function run() {
storageKey: 'widget',
token: $i?.token,
}),
- ...registerAsUiLib(components, (_root) => {
+ ...registerAsUiLib(components.value, (_root) => {
root.value = _root.value;
}),
}, {
diff --git a/packages/frontend/src/widgets/WidgetBirthdayFollowings.vue b/packages/frontend/src/widgets/WidgetBirthdayFollowings.vue
new file mode 100644
index 0000000000..7c4455516d
--- /dev/null
+++ b/packages/frontend/src/widgets/WidgetBirthdayFollowings.vue
@@ -0,0 +1,127 @@
+<!--
+SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-License-Identifier: AGPL-3.0-only
+-->
+
+<template>
+<MkContainer :showHeader="widgetProps.showHeader" class="mkw-bdayfollowings">
+ <template #icon><i class="ti ti-cake"></i></template>
+ <template #header>{{ i18n.ts._widgets.birthdayFollowings }}</template>
+
+ <div :class="$style.bdayFRoot">
+ <MkLoading v-if="fetching"/>
+ <div v-else-if="users.length > 0" :class="$style.bdayFGrid">
+ <MkAvatar v-for="user in users" :key="user.id" :user="user.followee" link preview></MkAvatar>
+ </div>
+ <div v-else :class="$style.bdayFFallback">
+ <img :src="infoImageUrl" class="_ghost" :class="$style.bdayFFallbackImage"/>
+ <div>{{ i18n.ts.nothing }}</div>
+ </div>
+ </div>
+</MkContainer>
+</template>
+
+<script lang="ts" setup>
+import { ref } from 'vue';
+import * as Misskey from 'misskey-js';
+import { useWidgetPropsManager, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js';
+import { GetFormResultType } from '@/scripts/form.js';
+import MkContainer from '@/components/MkContainer.vue';
+import * as os from '@/os.js';
+import { useInterval } from '@/scripts/use-interval.js';
+import { i18n } from '@/i18n.js';
+import { infoImageUrl } from '@/instance.js';
+import { $i } from '@/account.js';
+
+const name = i18n.ts._widgets.birthdayFollowings;
+
+const widgetPropsDef = {
+ showHeader: {
+ type: 'boolean' as const,
+ default: true,
+ },
+};
+
+type WidgetProps = GetFormResultType<typeof widgetPropsDef>;
+
+const props = defineProps<WidgetComponentProps<WidgetProps>>();
+const emit = defineEmits<WidgetComponentEmits<WidgetProps>>();
+
+const { widgetProps, configure } = useWidgetPropsManager(name,
+ widgetPropsDef,
+ props,
+ emit,
+);
+
+const users = ref<Misskey.entities.FollowingFolloweePopulated[]>([]);
+const fetching = ref(true);
+let lastFetchedAt = '1970-01-01';
+
+const fetch = () => {
+ if (!$i) {
+ users.value = [];
+ fetching.value = false;
+ return;
+ }
+
+ const lfAtD = new Date(lastFetchedAt);
+ lfAtD.setHours(0, 0, 0, 0);
+ const now = new Date();
+ now.setHours(0, 0, 0, 0);
+
+ if (now > lfAtD) {
+ os.api('users/following', {
+ limit: 18,
+ birthday: now.toISOString(),
+ userId: $i.id,
+ }).then(res => {
+ users.value = res;
+ fetching.value = false;
+ });
+
+ lastFetchedAt = now.toISOString();
+ }
+};
+
+useInterval(fetch, 1000 * 60, {
+ immediate: true,
+ afterMounted: true,
+});
+
+defineExpose<WidgetComponentExpose>({
+ name,
+ configure,
+ id: props.widget ? props.widget.id : null,
+});
+</script>
+
+<style lang="scss" module>
+.bdayFRoot {
+ overflow: hidden;
+ min-height: calc(calc(calc(50px * 3) - 8px) + calc(var(--margin) * 2));
+}
+.bdayFGrid {
+ display: grid;
+ grid-template-columns: repeat(6, 42px);
+ grid-template-rows: repeat(3, 42px);
+ place-content: center;
+ gap: 8px;
+ margin: var(--margin) auto;
+}
+
+.bdayFFallback {
+ height: 100%;
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ align-items: center;
+}
+
+.bdayFFallbackImage {
+ height: 96px;
+ width: auto;
+ max-width: 90%;
+ margin-bottom: 8px;
+ border-radius: var(--radius);
+}
+</style>
diff --git a/packages/frontend/src/widgets/WidgetButton.vue b/packages/frontend/src/widgets/WidgetButton.vue
index a7bdd4c49c..11082c1e3f 100644
--- a/packages/frontend/src/widgets/WidgetButton.vue
+++ b/packages/frontend/src/widgets/WidgetButton.vue
@@ -13,7 +13,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { Interpreter, Parser } from '@syuilo/aiscript';
-import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js';
+import { useWidgetPropsManager, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js';
import { GetFormResultType } from '@/scripts/form.js';
import * as os from '@/os.js';
import { createAiScriptEnv } from '@/scripts/aiscript/api.js';
diff --git a/packages/frontend/src/widgets/WidgetCalendar.vue b/packages/frontend/src/widgets/WidgetCalendar.vue
index 0ad166c6ba..b3f814a0a7 100644
--- a/packages/frontend/src/widgets/WidgetCalendar.vue
+++ b/packages/frontend/src/widgets/WidgetCalendar.vue
@@ -39,7 +39,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { ref } from 'vue';
-import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js';
+import { useWidgetPropsManager, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js';
import { GetFormResultType } from '@/scripts/form.js';
import { i18n } from '@/i18n.js';
import { useInterval } from '@/scripts/use-interval.js';
diff --git a/packages/frontend/src/widgets/WidgetClicker.vue b/packages/frontend/src/widgets/WidgetClicker.vue
index 2b3b391165..aa49269017 100644
--- a/packages/frontend/src/widgets/WidgetClicker.vue
+++ b/packages/frontend/src/widgets/WidgetClicker.vue
@@ -12,7 +12,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js';
+import { useWidgetPropsManager, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js';
import { GetFormResultType } from '@/scripts/form.js';
import MkContainer from '@/components/MkContainer.vue';
import MkClickerGame from '@/components/MkClickerGame.vue';
diff --git a/packages/frontend/src/widgets/WidgetClock.vue b/packages/frontend/src/widgets/WidgetClock.vue
index e4ea2c97dd..22f053db59 100644
--- a/packages/frontend/src/widgets/WidgetClock.vue
+++ b/packages/frontend/src/widgets/WidgetClock.vue
@@ -29,8 +29,8 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { } from 'vue';
-import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js';
+import { computed } from 'vue';
+import { useWidgetPropsManager, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js';
import { GetFormResultType } from '@/scripts/form.js';
import MkContainer from '@/components/MkContainer.vue';
import MkAnalogClock from '@/components/MkAnalogClock.vue';
@@ -134,15 +134,15 @@ const { widgetProps, configure } = useWidgetPropsManager(name,
emit,
);
-const tzAbbrev = $computed(() => (widgetProps.timezone === null
+const tzAbbrev = computed(() => (widgetProps.timezone === null
? timezones.find((tz) => tz.name.toLowerCase() === Intl.DateTimeFormat().resolvedOptions().timeZone.toLowerCase())?.abbrev
: timezones.find((tz) => tz.name.toLowerCase() === widgetProps.timezone)?.abbrev) ?? '?');
-const tzOffset = $computed(() => widgetProps.timezone === null
+const tzOffset = computed(() => widgetProps.timezone === null
? 0 - new Date().getTimezoneOffset()
: timezones.find((tz) => tz.name.toLowerCase() === widgetProps.timezone)?.offset ?? 0);
-const tzOffsetLabel = $computed(() => (tzOffset >= 0 ? '+' : '-') + Math.floor(tzOffset / 60).toString().padStart(2, '0') + ':' + (tzOffset % 60).toString().padStart(2, '0'));
+const tzOffsetLabel = computed(() => (tzOffset.value >= 0 ? '+' : '-') + Math.floor(tzOffset.value / 60).toString().padStart(2, '0') + ':' + (tzOffset.value % 60).toString().padStart(2, '0'));
defineExpose<WidgetComponentExpose>({
name,
diff --git a/packages/frontend/src/widgets/WidgetDigitalClock.vue b/packages/frontend/src/widgets/WidgetDigitalClock.vue
index 9ff5f8dcef..a4b90c49d3 100644
--- a/packages/frontend/src/widgets/WidgetDigitalClock.vue
+++ b/packages/frontend/src/widgets/WidgetDigitalClock.vue
@@ -14,7 +14,8 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js';
+import { computed } from 'vue';
+import { useWidgetPropsManager, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js';
import { GetFormResultType } from '@/scripts/form.js';
import { timezones } from '@/scripts/timezones.js';
import MkDigitalClock from '@/components/MkDigitalClock.vue';
@@ -63,15 +64,15 @@ const { widgetProps, configure } = useWidgetPropsManager(name,
emit,
);
-const tzAbbrev = $computed(() => (widgetProps.timezone === null
+const tzAbbrev = computed(() => (widgetProps.timezone === null
? timezones.find((tz) => tz.name.toLowerCase() === Intl.DateTimeFormat().resolvedOptions().timeZone.toLowerCase())?.abbrev
: timezones.find((tz) => tz.name.toLowerCase() === widgetProps.timezone)?.abbrev) ?? '?');
-const tzOffset = $computed(() => widgetProps.timezone === null
+const tzOffset = computed(() => widgetProps.timezone === null
? 0 - new Date().getTimezoneOffset()
: timezones.find((tz) => tz.name.toLowerCase() === widgetProps.timezone)?.offset ?? 0);
-const tzOffsetLabel = $computed(() => (tzOffset >= 0 ? '+' : '-') + Math.floor(tzOffset / 60).toString().padStart(2, '0') + ':' + (tzOffset % 60).toString().padStart(2, '0'));
+const tzOffsetLabel = computed(() => (tzOffset.value >= 0 ? '+' : '-') + Math.floor(tzOffset.value / 60).toString().padStart(2, '0') + ':' + (tzOffset.value % 60).toString().padStart(2, '0'));
defineExpose<WidgetComponentExpose>({
name,
diff --git a/packages/frontend/src/widgets/WidgetFederation.vue b/packages/frontend/src/widgets/WidgetFederation.vue
index e6ee590c15..605c24aaa3 100644
--- a/packages/frontend/src/widgets/WidgetFederation.vue
+++ b/packages/frontend/src/widgets/WidgetFederation.vue
@@ -26,7 +26,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { ref } from 'vue';
-import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js';
+import { useWidgetPropsManager, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js';
import { GetFormResultType } from '@/scripts/form.js';
import MkContainer from '@/components/MkContainer.vue';
import MkMiniChart from '@/components/MkMiniChart.vue';
diff --git a/packages/frontend/src/widgets/WidgetInstanceCloud.vue b/packages/frontend/src/widgets/WidgetInstanceCloud.vue
index 4ae77e86fc..0fc96c0d35 100644
--- a/packages/frontend/src/widgets/WidgetInstanceCloud.vue
+++ b/packages/frontend/src/widgets/WidgetInstanceCloud.vue
@@ -18,8 +18,8 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { } from 'vue';
-import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js';
+import { shallowRef } from 'vue';
+import { useWidgetPropsManager, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js';
import { GetFormResultType } from '@/scripts/form.js';
import MkContainer from '@/components/MkContainer.vue';
import MkTagCloud from '@/components/MkTagCloud.vue';
@@ -47,8 +47,8 @@ const { widgetProps, configure } = useWidgetPropsManager(name,
emit,
);
-let cloud = $shallowRef<InstanceType<typeof MkTagCloud> | null>();
-let activeInstances = $shallowRef(null);
+const cloud = shallowRef<InstanceType<typeof MkTagCloud> | null>();
+const activeInstances = shallowRef(null);
function onInstanceClick(i) {
os.pageWindow(`/instance-info/${i.host}`);
@@ -59,8 +59,8 @@ useInterval(() => {
sort: '+latestRequestReceivedAt',
limit: 25,
}).then(res => {
- activeInstances = res;
- if (cloud) cloud.update();
+ activeInstances.value = res;
+ if (cloud.value) cloud.value.update();
});
}, 1000 * 60 * 3, {
immediate: true,
diff --git a/packages/frontend/src/widgets/WidgetInstanceInfo.vue b/packages/frontend/src/widgets/WidgetInstanceInfo.vue
index 3fa811fa13..2133deb363 100644
--- a/packages/frontend/src/widgets/WidgetInstanceInfo.vue
+++ b/packages/frontend/src/widgets/WidgetInstanceInfo.vue
@@ -20,7 +20,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js';
+import { useWidgetPropsManager, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js';
import { GetFormResultType } from '@/scripts/form.js';
import { host } from '@/config.js';
import { instance } from '@/instance.js';
diff --git a/packages/frontend/src/widgets/WidgetJobQueue.vue b/packages/frontend/src/widgets/WidgetJobQueue.vue
index 09c27bfdec..c54682bb87 100644
--- a/packages/frontend/src/widgets/WidgetJobQueue.vue
+++ b/packages/frontend/src/widgets/WidgetJobQueue.vue
@@ -51,13 +51,14 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { onUnmounted, reactive } from 'vue';
-import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js';
+import { onUnmounted, reactive, ref } from 'vue';
+import { useWidgetPropsManager, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js';
import { GetFormResultType } from '@/scripts/form.js';
import { useStream } from '@/stream.js';
import number from '@/filters/number.js';
import * as sound from '@/scripts/sound.js';
import { deepClone } from '@/scripts/clone.js';
+import { defaultStore } from '@/store.js';
const name = 'jobQueue';
@@ -99,7 +100,18 @@ const current = reactive({
},
});
const prev = reactive({} as typeof current);
-const jammedSound = sound.setVolume(sound.getAudio('syuilo/queue-jammed'), 1);
+const jammedAudioBuffer = ref<AudioBuffer | null>(null);
+const jammedSoundNodePlaying = ref<boolean>(false);
+
+if (defaultStore.state.sound_masterVolume) {
+ sound.loadAudio({
+ type: 'syuilo/queue-jammed',
+ volume: 1,
+ }).then(buf => {
+ if (!buf) throw new Error('[WidgetJobQueue] Failed to initialize AudioBuffer');
+ jammedAudioBuffer.value = buf;
+ });
+}
for (const domain of ['inbox', 'deliver']) {
prev[domain] = deepClone(current[domain]);
@@ -113,8 +125,13 @@ const onStats = (stats) => {
current[domain].waiting = stats[domain].waiting;
current[domain].delayed = stats[domain].delayed;
- if (current[domain].waiting > 0 && widgetProps.sound && jammedSound.paused) {
- jammedSound.play();
+ if (current[domain].waiting > 0 && widgetProps.sound && jammedAudioBuffer.value && !jammedSoundNodePlaying.value) {
+ const soundNode = sound.createSourceNode(jammedAudioBuffer.value, 1);
+ if (soundNode) {
+ jammedSoundNodePlaying.value = true;
+ soundNode.onended = () => jammedSoundNodePlaying.value = false;
+ soundNode.start();
+ }
}
}
};
diff --git a/packages/frontend/src/widgets/WidgetMemo.vue b/packages/frontend/src/widgets/WidgetMemo.vue
index c2d5ef566c..8e9e67ade5 100644
--- a/packages/frontend/src/widgets/WidgetMemo.vue
+++ b/packages/frontend/src/widgets/WidgetMemo.vue
@@ -17,7 +17,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { ref, watch } from 'vue';
-import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js';
+import { useWidgetPropsManager, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js';
import { GetFormResultType } from '@/scripts/form.js';
import MkContainer from '@/components/MkContainer.vue';
import { defaultStore } from '@/store.js';
diff --git a/packages/frontend/src/widgets/WidgetNotifications.vue b/packages/frontend/src/widgets/WidgetNotifications.vue
index 6e38b8308c..e858741aa1 100644
--- a/packages/frontend/src/widgets/WidgetNotifications.vue
+++ b/packages/frontend/src/widgets/WidgetNotifications.vue
@@ -17,7 +17,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { defineAsyncComponent } from 'vue';
-import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js';
+import { useWidgetPropsManager, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js';
import { GetFormResultType } from '@/scripts/form.js';
import MkContainer from '@/components/MkContainer.vue';
import XNotifications from '@/components/MkNotifications.vue';
diff --git a/packages/frontend/src/widgets/WidgetOnlineUsers.vue b/packages/frontend/src/widgets/WidgetOnlineUsers.vue
index 46fe991f37..0a6fec7f2e 100644
--- a/packages/frontend/src/widgets/WidgetOnlineUsers.vue
+++ b/packages/frontend/src/widgets/WidgetOnlineUsers.vue
@@ -15,7 +15,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { ref } from 'vue';
-import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js';
+import { useWidgetPropsManager, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js';
import { GetFormResultType } from '@/scripts/form.js';
import * as os from '@/os.js';
import { useInterval } from '@/scripts/use-interval.js';
diff --git a/packages/frontend/src/widgets/WidgetPhotos.vue b/packages/frontend/src/widgets/WidgetPhotos.vue
index c07f2dd262..808f023174 100644
--- a/packages/frontend/src/widgets/WidgetPhotos.vue
+++ b/packages/frontend/src/widgets/WidgetPhotos.vue
@@ -23,7 +23,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { onUnmounted, ref } from 'vue';
-import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js';
+import { useWidgetPropsManager, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js';
import { GetFormResultType } from '@/scripts/form.js';
import { useStream } from '@/stream.js';
import { getStaticImageUrl } from '@/scripts/media-proxy.js';
diff --git a/packages/frontend/src/widgets/WidgetPostForm.vue b/packages/frontend/src/widgets/WidgetPostForm.vue
index 320b47a4ff..9979ae256e 100644
--- a/packages/frontend/src/widgets/WidgetPostForm.vue
+++ b/packages/frontend/src/widgets/WidgetPostForm.vue
@@ -9,7 +9,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { } from 'vue';
-import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js';
+import { useWidgetPropsManager, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js';
import { GetFormResultType } from '@/scripts/form.js';
import MkPostForm from '@/components/MkPostForm.vue';
diff --git a/packages/frontend/src/widgets/WidgetProfile.vue b/packages/frontend/src/widgets/WidgetProfile.vue
index fc54af2d71..3ff57bab86 100644
--- a/packages/frontend/src/widgets/WidgetProfile.vue
+++ b/packages/frontend/src/widgets/WidgetProfile.vue
@@ -22,7 +22,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js';
+import { useWidgetPropsManager, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js';
import { GetFormResultType } from '@/scripts/form.js';
import { $i } from '@/account.js';
import { userPage } from '@/filters/user.js';
diff --git a/packages/frontend/src/widgets/WidgetRss.vue b/packages/frontend/src/widgets/WidgetRss.vue
index 1ada9f4be8..a718548731 100644
--- a/packages/frontend/src/widgets/WidgetRss.vue
+++ b/packages/frontend/src/widgets/WidgetRss.vue
@@ -24,7 +24,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { ref, watch, computed } from 'vue';
-import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js';
+import { useWidgetPropsManager, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js';
import { GetFormResultType } from '@/scripts/form.js';
import MkContainer from '@/components/MkContainer.vue';
import { url as base } from '@/config.js';
@@ -72,7 +72,7 @@ const fetchEndpoint = computed(() => {
url.searchParams.set('url', widgetProps.url);
return url;
});
-let intervalClear = $ref<(() => void) | undefined>();
+const intervalClear = ref<(() => void) | undefined>();
const tick = () => {
if (document.visibilityState === 'hidden' && rawItems.value.length !== 0) return;
@@ -87,10 +87,10 @@ const tick = () => {
watch(() => fetchEndpoint, tick);
watch(() => widgetProps.refreshIntervalSec, () => {
- if (intervalClear) {
- intervalClear();
+ if (intervalClear.value) {
+ intervalClear.value();
}
- intervalClear = useInterval(tick, Math.max(10000, widgetProps.refreshIntervalSec * 1000), {
+ intervalClear.value = useInterval(tick, Math.max(10000, widgetProps.refreshIntervalSec * 1000), {
immediate: true,
afterMounted: true,
});
diff --git a/packages/frontend/src/widgets/WidgetRssTicker.vue b/packages/frontend/src/widgets/WidgetRssTicker.vue
index 790f94f7c3..607bb2f0ab 100644
--- a/packages/frontend/src/widgets/WidgetRssTicker.vue
+++ b/packages/frontend/src/widgets/WidgetRssTicker.vue
@@ -28,7 +28,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { ref, watch, computed } from 'vue';
-import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js';
+import { useWidgetPropsManager, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js';
import MarqueeText from '@/components/MkMarquee.vue';
import { GetFormResultType } from '@/scripts/form.js';
import MkContainer from '@/components/MkContainer.vue';
@@ -101,9 +101,9 @@ const fetchEndpoint = computed(() => {
url.searchParams.set('url', widgetProps.url);
return url;
});
-let intervalClear = $ref<(() => void) | undefined>();
+const intervalClear = ref<(() => void) | undefined>();
-let key = $ref(0);
+const key = ref(0);
const tick = () => {
if (document.visibilityState === 'hidden' && rawItems.value.length !== 0) return;
@@ -113,16 +113,16 @@ const tick = () => {
.then(feed => {
rawItems.value = feed.items ?? [];
fetching.value = false;
- key++;
+ key.value++;
});
};
watch(() => fetchEndpoint, tick);
watch(() => widgetProps.refreshIntervalSec, () => {
- if (intervalClear) {
- intervalClear();
+ if (intervalClear.value) {
+ intervalClear.value();
}
- intervalClear = useInterval(tick, Math.max(10000, widgetProps.refreshIntervalSec * 1000), {
+ intervalClear.value = useInterval(tick, Math.max(10000, widgetProps.refreshIntervalSec * 1000), {
immediate: true,
afterMounted: true,
});
diff --git a/packages/frontend/src/widgets/WidgetSearch.vue b/packages/frontend/src/widgets/WidgetSearch.vue
index 979341e1ae..c114707b23 100644
--- a/packages/frontend/src/widgets/WidgetSearch.vue
+++ b/packages/frontend/src/widgets/WidgetSearch.vue
@@ -15,8 +15,8 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { computed, defineAsyncComponent, onMounted } from 'vue';
-import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js';
+import { defineAsyncComponent, ref } from 'vue';
+import { useWidgetPropsManager, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js';
import MkInput from '@/components/MkInput.vue';
import MkContainer from '@/components/MkContainer.vue';
import { i18n } from '@/i18n.js';
@@ -54,14 +54,12 @@ function onInputKeydown(evt: KeyboardEvent) {
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);
+let key = ref(0);
+let searchQuery = ref('');
+let notePagination = ref();
+let isLocalOnly = ref(false);
+let order = ref(true);
+let filetype = ref<null | string>(null);
function options(ev) {
os.popupMenu([{
@@ -74,7 +72,7 @@ function options(ev) {
icon: 'ph-image ph-bold ph-lg',
text: 'With Images',
action: () => {
- filetype = 'image';
+ filetype.value = 'image';
},
},
{
@@ -82,7 +80,7 @@ function options(ev) {
icon: 'ph-music-notes-simple ph-bold ph-lg',
text: 'With Audios',
action: () => {
- filetype = 'audio';
+ filetype.value = 'audio';
},
},
{
@@ -90,20 +88,14 @@ function options(ev) {
icon: 'ph-video ph-bold ph-lg',
text: 'With Videos',
action: () => {
- filetype = 'video';
+ filetype.value = 'video';
},
}],
}], ev.currentTarget ?? ev.target);
}
-function selectUser() {
- os.selectUser().then(_user => {
- user = _user;
- });
-}
-
async function search() {
- const query = searchQuery.toString().trim();
+ const query = searchQuery.value.toString().trim();
if (query == null || query === '') return;
@@ -125,24 +117,24 @@ async function search() {
return;
}
- notePagination = {
+ notePagination.value = {
endpoint: 'notes/search',
limit: 10,
params: {
query: searchQuery,
- userId: user ? user.id : null,
- order: order ? 'desc' : 'asc',
- filetype: filetype,
+ userId: null,
+ order: order.value ? 'desc' : 'asc',
+ filetype: filetype.value,
},
};
- if (isLocalOnly) notePagination.params.host = '.';
+ if (isLocalOnly.value) notePagination.value.params.host = '.';
- key++;
+ key.value++;
os.popup(defineAsyncComponent(() => import('@/components/SkSearchResultWindow.vue')), {
- noteKey: key,
- notePagination: notePagination,
+ noteKey: key.value,
+ notePagination: notePagination.value,
}, {
}, 'closed');
}
diff --git a/packages/frontend/src/widgets/WidgetSlideshow.vue b/packages/frontend/src/widgets/WidgetSlideshow.vue
index 82b6246add..eccb9a00bf 100644
--- a/packages/frontend/src/widgets/WidgetSlideshow.vue
+++ b/packages/frontend/src/widgets/WidgetSlideshow.vue
@@ -18,7 +18,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { onMounted, ref, shallowRef } from 'vue';
-import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js';
+import { useWidgetPropsManager, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js';
import { GetFormResultType } from '@/scripts/form.js';
import * as os from '@/os.js';
import { useInterval } from '@/scripts/use-interval.js';
diff --git a/packages/frontend/src/widgets/WidgetTimeline.vue b/packages/frontend/src/widgets/WidgetTimeline.vue
index 0ebffa105e..070466f476 100644
--- a/packages/frontend/src/widgets/WidgetTimeline.vue
+++ b/packages/frontend/src/widgets/WidgetTimeline.vue
@@ -36,7 +36,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { ref } from 'vue';
-import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js';
+import { useWidgetPropsManager, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js';
import { GetFormResultType } from '@/scripts/form.js';
import * as os from '@/os.js';
import MkContainer from '@/components/MkContainer.vue';
@@ -136,7 +136,7 @@ const choose = async (ev) => {
text: i18n.ts._timelines.global,
icon: 'ph-globe-hemisphere-west ph-bold ph-lg',
action: () => { setSrc('global'); },
- }, antennaItems.length > 0 ? null : undefined, ...antennaItems, listItems.length > 0 ? null : undefined, ...listItems], ev.currentTarget ?? ev.target).then(() => {
+ }, antennaItems.length > 0 ? { type: 'divider' } : undefined, ...antennaItems, listItems.length > 0 ? { type: 'divider' } : undefined, ...listItems], ev.currentTarget ?? ev.target).then(() => {
menuOpened.value = false;
});
};
diff --git a/packages/frontend/src/widgets/WidgetTrends.vue b/packages/frontend/src/widgets/WidgetTrends.vue
index ea7b9078f3..738cd70b03 100644
--- a/packages/frontend/src/widgets/WidgetTrends.vue
+++ b/packages/frontend/src/widgets/WidgetTrends.vue
@@ -25,7 +25,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { ref } from 'vue';
-import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js';
+import { useWidgetPropsManager, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js';
import { GetFormResultType } from '@/scripts/form.js';
import MkContainer from '@/components/MkContainer.vue';
import MkMiniChart from '@/components/MkMiniChart.vue';
diff --git a/packages/frontend/src/widgets/WidgetUnixClock.vue b/packages/frontend/src/widgets/WidgetUnixClock.vue
index 33585cd721..35f29b5e21 100644
--- a/packages/frontend/src/widgets/WidgetUnixClock.vue
+++ b/packages/frontend/src/widgets/WidgetUnixClock.vue
@@ -17,7 +17,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { onUnmounted, ref, watch } from 'vue';
-import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js';
+import { useWidgetPropsManager, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js';
import { GetFormResultType } from '@/scripts/form.js';
const name = 'unixClock';
diff --git a/packages/frontend/src/widgets/WidgetUserList.vue b/packages/frontend/src/widgets/WidgetUserList.vue
index 159318c2ca..f5c1d2d3a3 100644
--- a/packages/frontend/src/widgets/WidgetUserList.vue
+++ b/packages/frontend/src/widgets/WidgetUserList.vue
@@ -24,7 +24,8 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js';
+import { ref } from 'vue';
+import { useWidgetPropsManager, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js';
import { GetFormResultType } from '@/scripts/form.js';
import MkContainer from '@/components/MkContainer.vue';
import * as os from '@/os.js';
@@ -57,9 +58,9 @@ const { widgetProps, configure, save } = useWidgetPropsManager(name,
emit,
);
-let list = $ref();
-let users = $ref([]);
-let fetching = $ref(true);
+const list = ref();
+const users = ref([]);
+const fetching = ref(true);
async function chooseList() {
const lists = await os.api('users/lists/list');
@@ -79,19 +80,19 @@ async function chooseList() {
const fetch = () => {
if (widgetProps.listId == null) {
- fetching = false;
+ fetching.value = false;
return;
}
os.api('users/lists/show', {
listId: widgetProps.listId,
}).then(_list => {
- list = _list;
+ list.value = _list;
os.api('users/show', {
- userIds: list.userIds,
+ userIds: list.value.userIds,
}).then(_users => {
- users = _users;
- fetching = false;
+ users.value = _users;
+ fetching.value = false;
});
});
};
diff --git a/packages/frontend/src/widgets/index.ts b/packages/frontend/src/widgets/index.ts
index ae3bd09c86..b783d783bc 100644
--- a/packages/frontend/src/widgets/index.ts
+++ b/packages/frontend/src/widgets/index.ts
@@ -34,6 +34,7 @@ export default function(app: App) {
app.component('WidgetUserList', defineAsyncComponent(() => import('./WidgetUserList.vue')));
app.component('WidgetClicker', defineAsyncComponent(() => import('./WidgetClicker.vue')));
app.component('WidgetSearch', defineAsyncComponent(() => import('./WidgetSearch.vue')));
+ app.component('WidgetBirthdayFollowings', defineAsyncComponent(() => import('./WidgetBirthdayFollowings.vue')));
}
export const widgets = [
@@ -65,4 +66,5 @@ export const widgets = [
'userList',
'clicker',
'search',
+ 'birthdayFollowings',
];
diff --git a/packages/frontend/src/widgets/server-metric/cpu-mem.vue b/packages/frontend/src/widgets/server-metric/cpu-mem.vue
index c656d75429..9196ae209f 100644
--- a/packages/frontend/src/widgets/server-metric/cpu-mem.vue
+++ b/packages/frontend/src/widgets/server-metric/cpu-mem.vue
@@ -75,7 +75,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { onMounted, onBeforeUnmount } from 'vue';
+import { onMounted, onBeforeUnmount, ref } from 'vue';
import { v4 as uuid } from 'uuid';
const props = defineProps<{
@@ -83,23 +83,23 @@ const props = defineProps<{
meta: any
}>();
-let viewBoxX: number = $ref(50);
-let viewBoxY: number = $ref(30);
-let stats: any[] = $ref([]);
+const viewBoxX = ref<number>(50);
+const viewBoxY = ref<number>(30);
+const stats = ref<any[]>([]);
const cpuGradientId = uuid();
const cpuMaskId = uuid();
const memGradientId = uuid();
const memMaskId = uuid();
-let cpuPolylinePoints: string = $ref('');
-let memPolylinePoints: string = $ref('');
-let cpuPolygonPoints: string = $ref('');
-let memPolygonPoints: string = $ref('');
-let cpuHeadX: any = $ref(null);
-let cpuHeadY: any = $ref(null);
-let memHeadX: any = $ref(null);
-let memHeadY: any = $ref(null);
-let cpuP: string = $ref('');
-let memP: string = $ref('');
+const cpuPolylinePoints = ref<string>('');
+const memPolylinePoints = ref<string>('');
+const cpuPolygonPoints = ref<string>('');
+const memPolygonPoints = ref<string>('');
+const cpuHeadX = ref<any>(null);
+const cpuHeadY = ref<any>(null);
+const memHeadX = ref<any>(null);
+const memHeadY = ref<any>(null);
+const cpuP = ref<string>('');
+const memP = ref<string>('');
onMounted(() => {
props.connection.on('stats', onStats);
@@ -115,24 +115,24 @@ onBeforeUnmount(() => {
});
function onStats(connStats) {
- stats.push(connStats);
- if (stats.length > 50) stats.shift();
+ stats.value.push(connStats);
+ if (stats.value.length > 50) stats.value.shift();
- let cpuPolylinePointsStats = stats.map((s, i) => [viewBoxX - ((stats.length - 1) - i), (1 - s.cpu) * viewBoxY]);
- let memPolylinePointsStats = stats.map((s, i) => [viewBoxX - ((stats.length - 1) - i), (1 - (s.mem.active / props.meta.mem.total)) * viewBoxY]);
- cpuPolylinePoints = cpuPolylinePointsStats.map(xy => `${xy[0]},${xy[1]}`).join(' ');
- memPolylinePoints = memPolylinePointsStats.map(xy => `${xy[0]},${xy[1]}`).join(' ');
+ let cpuPolylinePointsStats = stats.value.map((s, i) => [viewBoxX.value - ((stats.value.length - 1) - i), (1 - s.cpu) * viewBoxY.value]);
+ let memPolylinePointsStats = stats.value.map((s, i) => [viewBoxX.value - ((stats.value.length - 1) - i), (1 - (s.mem.active / props.meta.mem.total)) * viewBoxY.value]);
+ cpuPolylinePoints.value = cpuPolylinePointsStats.map(xy => `${xy[0]},${xy[1]}`).join(' ');
+ memPolylinePoints.value = memPolylinePointsStats.map(xy => `${xy[0]},${xy[1]}`).join(' ');
- cpuPolygonPoints = `${viewBoxX - (stats.length - 1)},${viewBoxY} ${cpuPolylinePoints} ${viewBoxX},${viewBoxY}`;
- memPolygonPoints = `${viewBoxX - (stats.length - 1)},${viewBoxY} ${memPolylinePoints} ${viewBoxX},${viewBoxY}`;
+ cpuPolygonPoints.value = `${viewBoxX.value - (stats.value.length - 1)},${viewBoxY.value} ${cpuPolylinePoints.value} ${viewBoxX.value},${viewBoxY.value}`;
+ memPolygonPoints.value = `${viewBoxX.value - (stats.value.length - 1)},${viewBoxY.value} ${memPolylinePoints.value} ${viewBoxX.value},${viewBoxY.value}`;
- cpuHeadX = cpuPolylinePointsStats.at(-1)![0];
- cpuHeadY = cpuPolylinePointsStats.at(-1)![1];
- memHeadX = memPolylinePointsStats.at(-1)![0];
- memHeadY = memPolylinePointsStats.at(-1)![1];
+ cpuHeadX.value = cpuPolylinePointsStats.at(-1)![0];
+ cpuHeadY.value = cpuPolylinePointsStats.at(-1)![1];
+ memHeadX.value = memPolylinePointsStats.at(-1)![0];
+ memHeadY.value = memPolylinePointsStats.at(-1)![1];
- cpuP = (connStats.cpu * 100).toFixed(0);
- memP = (connStats.mem.active / props.meta.mem.total * 100).toFixed(0);
+ cpuP.value = (connStats.cpu * 100).toFixed(0);
+ memP.value = (connStats.mem.active / props.meta.mem.total * 100).toFixed(0);
}
function onStatsLog(statsLog) {
diff --git a/packages/frontend/src/widgets/server-metric/cpu.vue b/packages/frontend/src/widgets/server-metric/cpu.vue
index 65da16a632..cffbdb27ce 100644
--- a/packages/frontend/src/widgets/server-metric/cpu.vue
+++ b/packages/frontend/src/widgets/server-metric/cpu.vue
@@ -15,7 +15,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { onMounted, onBeforeUnmount } from 'vue';
+import { onMounted, onBeforeUnmount, ref } from 'vue';
import XPie from './pie.vue';
const props = defineProps<{
@@ -23,10 +23,10 @@ const props = defineProps<{
meta: any
}>();
-let usage: number = $ref(0);
+const usage = ref<number>(0);
function onStats(stats) {
- usage = stats.cpu;
+ usage.value = stats.cpu;
}
onMounted(() => {
diff --git a/packages/frontend/src/widgets/server-metric/disk.vue b/packages/frontend/src/widgets/server-metric/disk.vue
index b9774da0cf..18f8560265 100644
--- a/packages/frontend/src/widgets/server-metric/disk.vue
+++ b/packages/frontend/src/widgets/server-metric/disk.vue
@@ -16,7 +16,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { } from 'vue';
+import { computed } from 'vue';
import XPie from './pie.vue';
import bytes from '@/filters/bytes.js';
@@ -24,10 +24,10 @@ const props = defineProps<{
meta: any; // TODO
}>();
-const usage = $computed(() => props.meta.fs.used / props.meta.fs.total);
-const total = $computed(() => props.meta.fs.total);
-const used = $computed(() => props.meta.fs.used);
-const available = $computed(() => props.meta.fs.total - props.meta.fs.used);
+const usage = computed(() => props.meta.fs.used / props.meta.fs.total);
+const total = computed(() => props.meta.fs.total);
+const used = computed(() => props.meta.fs.used);
+const available = computed(() => props.meta.fs.total - props.meta.fs.used);
</script>
<style lang="scss" scoped>
diff --git a/packages/frontend/src/widgets/server-metric/mem.vue b/packages/frontend/src/widgets/server-metric/mem.vue
index 5a57ef6e1e..118fd68fe8 100644
--- a/packages/frontend/src/widgets/server-metric/mem.vue
+++ b/packages/frontend/src/widgets/server-metric/mem.vue
@@ -16,7 +16,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { onMounted, onBeforeUnmount } from 'vue';
+import { onMounted, onBeforeUnmount, ref } from 'vue';
import XPie from './pie.vue';
import bytes from '@/filters/bytes.js';
@@ -25,16 +25,16 @@ const props = defineProps<{
meta: any
}>();
-let usage: number = $ref(0);
-let total: number = $ref(0);
-let used: number = $ref(0);
-let free: number = $ref(0);
+const usage = ref<number>(0);
+const total = ref<number>(0);
+const used = ref<number>(0);
+const free = ref<number>(0);
function onStats(stats) {
- usage = stats.mem.active / props.meta.mem.total;
- total = props.meta.mem.total;
- used = stats.mem.active;
- free = total - used;
+ usage.value = stats.mem.active / props.meta.mem.total;
+ total.value = props.meta.mem.total;
+ used.value = stats.mem.active;
+ free.value = total.value - used.value;
}
onMounted(() => {
diff --git a/packages/frontend/src/widgets/server-metric/net.vue b/packages/frontend/src/widgets/server-metric/net.vue
index 5593128660..e6a8bfc22a 100644
--- a/packages/frontend/src/widgets/server-metric/net.vue
+++ b/packages/frontend/src/widgets/server-metric/net.vue
@@ -49,7 +49,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { onMounted, onBeforeUnmount } from 'vue';
+import { onMounted, onBeforeUnmount, ref } from 'vue';
import bytes from '@/filters/bytes.js';
const props = defineProps<{
@@ -57,19 +57,19 @@ const props = defineProps<{
meta: any
}>();
-let viewBoxX: number = $ref(50);
-let viewBoxY: number = $ref(30);
-let stats: any[] = $ref([]);
-let inPolylinePoints: string = $ref('');
-let outPolylinePoints: string = $ref('');
-let inPolygonPoints: string = $ref('');
-let outPolygonPoints: string = $ref('');
-let inHeadX: any = $ref(null);
-let inHeadY: any = $ref(null);
-let outHeadX: any = $ref(null);
-let outHeadY: any = $ref(null);
-let inRecent: number = $ref(0);
-let outRecent: number = $ref(0);
+const viewBoxX = ref<number>(50);
+const viewBoxY = ref<number>(30);
+const stats = ref<any[]>([]);
+const inPolylinePoints = ref<string>('');
+const outPolylinePoints = ref<string>('');
+const inPolygonPoints = ref<string>('');
+const outPolygonPoints = ref<string>('');
+const inHeadX = ref<any>(null);
+const inHeadY = ref<any>(null);
+const outHeadX = ref<any>(null);
+const outHeadY = ref<any>(null);
+const inRecent = ref<number>(0);
+const outRecent = ref<number>(0);
onMounted(() => {
props.connection.on('stats', onStats);
@@ -85,27 +85,27 @@ onBeforeUnmount(() => {
});
function onStats(connStats) {
- stats.push(connStats);
- if (stats.length > 50) stats.shift();
+ stats.value.push(connStats);
+ if (stats.value.length > 50) stats.value.shift();
- const inPeak = Math.max(1024 * 64, Math.max(...stats.map(s => s.net.rx)));
- const outPeak = Math.max(1024 * 64, Math.max(...stats.map(s => s.net.tx)));
+ const inPeak = Math.max(1024 * 64, Math.max(...stats.value.map(s => s.net.rx)));
+ const outPeak = Math.max(1024 * 64, Math.max(...stats.value.map(s => s.net.tx)));
- let inPolylinePointsStats = stats.map((s, i) => [viewBoxX - ((stats.length - 1) - i), (1 - (s.net.rx / inPeak)) * viewBoxY]);
- let outPolylinePointsStats = stats.map((s, i) => [viewBoxX - ((stats.length - 1) - i), (1 - (s.net.tx / outPeak)) * viewBoxY]);
- inPolylinePoints = inPolylinePointsStats.map(xy => `${xy[0]},${xy[1]}`).join(' ');
- outPolylinePoints = outPolylinePointsStats.map(xy => `${xy[0]},${xy[1]}`).join(' ');
+ let inPolylinePointsStats = stats.value.map((s, i) => [viewBoxX.value - ((stats.value.length - 1) - i), (1 - (s.net.rx / inPeak)) * viewBoxY.value]);
+ let outPolylinePointsStats = stats.value.map((s, i) => [viewBoxX.value - ((stats.value.length - 1) - i), (1 - (s.net.tx / outPeak)) * viewBoxY.value]);
+ inPolylinePoints.value = inPolylinePointsStats.map(xy => `${xy[0]},${xy[1]}`).join(' ');
+ outPolylinePoints.value = outPolylinePointsStats.map(xy => `${xy[0]},${xy[1]}`).join(' ');
- inPolygonPoints = `${viewBoxX - (stats.length - 1)},${viewBoxY} ${inPolylinePoints} ${viewBoxX},${viewBoxY}`;
- outPolygonPoints = `${viewBoxX - (stats.length - 1)},${viewBoxY} ${outPolylinePoints} ${viewBoxX},${viewBoxY}`;
+ inPolygonPoints.value = `${viewBoxX.value - (stats.value.length - 1)},${viewBoxY.value} ${inPolylinePoints.value} ${viewBoxX.value},${viewBoxY.value}`;
+ outPolygonPoints.value = `${viewBoxX.value - (stats.value.length - 1)},${viewBoxY.value} ${outPolylinePoints.value} ${viewBoxX.value},${viewBoxY.value}`;
- inHeadX = inPolylinePointsStats.at(-1)![0];
- inHeadY = inPolylinePointsStats.at(-1)![1];
- outHeadX = outPolylinePointsStats.at(-1)![0];
- outHeadY = outPolylinePointsStats.at(-1)![1];
+ inHeadX.value = inPolylinePointsStats.at(-1)![0];
+ inHeadY.value = inPolylinePointsStats.at(-1)![1];
+ outHeadX.value = outPolylinePointsStats.at(-1)![0];
+ outHeadY.value = outPolylinePointsStats.at(-1)![1];
- inRecent = connStats.net.rx;
- outRecent = connStats.net.tx;
+ inRecent.value = connStats.net.rx;
+ outRecent.value = connStats.net.tx;
}
function onStatsLog(statsLog) {
diff --git a/packages/frontend/src/widgets/server-metric/pie.vue b/packages/frontend/src/widgets/server-metric/pie.vue
index c8a1496101..fd18a6a4f2 100644
--- a/packages/frontend/src/widgets/server-metric/pie.vue
+++ b/packages/frontend/src/widgets/server-metric/pie.vue
@@ -28,7 +28,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { } from 'vue';
+import { computed } from 'vue';
const props = defineProps<{
value: number;
@@ -36,8 +36,8 @@ const props = defineProps<{
const r = 0.45;
-const color = $computed(() => `hsl(${180 - (props.value * 180)}, 80%, 70%)`);
-const strokeDashoffset = $computed(() => (1 - props.value) * (Math.PI * (r * 2)));
+const color = computed(() => `hsl(${180 - (props.value * 180)}, 80%, 70%)`);
+const strokeDashoffset = computed(() => (1 - props.value) * (Math.PI * (r * 2)));
</script>
<style lang="scss" module>