diff --git a/index.html b/index.html index e2641312..ba6c8651 100644 --- a/index.html +++ b/index.html @@ -4,8 +4,6 @@ Akkoma - - diff --git a/src/boot/after_store.js b/src/boot/after_store.js index 0f95237c..30d46da9 100644 --- a/src/boot/after_store.js +++ b/src/boot/after_store.js @@ -324,6 +324,9 @@ const getNodeInfo = async ({ store }) => { : federation.enabled }) + store.dispatch('setInstanceOption', { name: 'publicTimelineVisibility', value: metadata.publicTimelineVisibility }) + store.dispatch('setInstanceOption', { name: 'federatedTimelineAvailable', value: metadata.federatedTimelineAvailable }) + const accountActivationRequired = metadata.accountActivationRequired store.dispatch('setInstanceOption', { name: 'accountActivationRequired', value: accountActivationRequired }) @@ -398,9 +401,6 @@ const afterStoreSetup = async ({ store, i18n }) => { ]) // Start fetching things that don't need to block the UI - store.dispatch('fetchMutes') - store.dispatch('startFetchingAnnouncements') - store.dispatch('startFetchingReports') getTOS({ store }) getStickers({ store }) diff --git a/src/components/desktop_nav/desktop_nav.js b/src/components/desktop_nav/desktop_nav.js index f4900c38..d7538f5b 100644 --- a/src/components/desktop_nav/desktop_nav.js +++ b/src/components/desktop_nav/desktop_nav.js @@ -1,6 +1,11 @@ import SearchBar from 'components/search_bar/search_bar.vue' import ConfirmModal from '../confirm_modal/confirm_modal.vue' import { library } from '@fortawesome/fontawesome-svg-core' +import { + publicTimelineVisible, + federatedTimelineVisible, + bubbleTimelineVisible, +} from '../../lib/timeline_visibility' import { faSignInAlt, faSignOutAlt, @@ -19,6 +24,7 @@ import { faInfoCircle, faUserTie } from '@fortawesome/free-solid-svg-icons' +import { mapState } from 'vuex' library.add( faSignInAlt, @@ -103,7 +109,12 @@ export default { }, showBubbleTimeline () { return this.$store.state.instance.localBubbleInstances.length > 0 - } + }, + ...mapState({ + publicTimelineVisible, + federatedTimelineVisible, + bubbleTimelineVisible, + }) }, methods: { scrollToTop () { diff --git a/src/components/desktop_nav/desktop_nav.vue b/src/components/desktop_nav/desktop_nav.vue index 92d3fa5b..f50d1b3e 100644 --- a/src/components/desktop_nav/desktop_nav.vue +++ b/src/components/desktop_nav/desktop_nav.vue @@ -46,6 +46,7 @@ @@ -69,6 +70,7 @@ { + const allEmojis = state.instance.emoji.concat(state.instance.customEmoji) + return allEmojis.find(emoji => emoji.replacement === replacement) +} + const EmojiReactions = { name: 'EmojiReactions', components: { @@ -54,6 +59,8 @@ const EmojiReactions = { }, reactWith (emoji) { this.$store.dispatch('reactWithEmoji', { id: this.status.id, emoji }) + const emojiObject = findEmojiByReplacement(this.$store.state, emoji) + this.$store.commit('emojiUsed', emojiObject) }, unreact (emoji) { this.$store.dispatch('unreactWithEmoji', { id: this.status.id, emoji }) diff --git a/src/components/emoji_reactions/emoji_reactions.vue b/src/components/emoji_reactions/emoji_reactions.vue index 3fe81f77..599611b2 100644 --- a/src/components/emoji_reactions/emoji_reactions.vue +++ b/src/components/emoji_reactions/emoji_reactions.vue @@ -18,7 +18,6 @@ :src="reaction.url" :title="reaction.name" class="reaction-emoji" - width="2.55em" height="2.55em" > {{ reaction.count }} @@ -50,6 +49,7 @@ display: flex; margin-top: 0.25em; flex-wrap: wrap; + container-type: inline-size; } .unicode-emoji { @@ -65,7 +65,8 @@ justify-content: center; box-sizing: border-box; .reaction-emoji { - width: 2.55em !important; + width: auto; + max-width: 96cqw; height: 2.55em !important; margin-right: 0.25em; } diff --git a/src/components/extra_buttons/extra_buttons.js b/src/components/extra_buttons/extra_buttons.js index 5eb98264..14c605fb 100644 --- a/src/components/extra_buttons/extra_buttons.js +++ b/src/components/extra_buttons/extra_buttons.js @@ -15,6 +15,7 @@ import { faBookmark as faBookmarkReg, faFlag } from '@fortawesome/free-regular-svg-icons' +import { mapState } from 'vuex' library.add( faEllipsisH, @@ -191,7 +192,7 @@ const ExtraButtons = { isEdited () { return this.status.edited_at !== null }, - editingAvailable () { return this.$store.state.instance.editingAvailable } + editingAvailable () { return this.$store.state.instance.editingAvailable }, } } diff --git a/src/components/nav_panel/nav_panel.js b/src/components/nav_panel/nav_panel.js index e63be37e..13c22924 100644 --- a/src/components/nav_panel/nav_panel.js +++ b/src/components/nav_panel/nav_panel.js @@ -53,7 +53,7 @@ const NavPanel = { ...mapState({ currentUser: state => state.users.currentUser, privateMode: state => state.instance.private, - federating: state => state.instance.federating + federating: state => state.instance.federating, }), ...mapGetters(['unreadAnnouncementCount']), followRequestCount () { diff --git a/src/components/poll/poll_form.js b/src/components/poll/poll_form.js index e30645c3..89f01eb4 100644 --- a/src/components/poll/poll_form.js +++ b/src/components/poll/poll_form.js @@ -103,9 +103,9 @@ export default { convertExpiryFromUnit (unit, amount) { // Note: we want seconds and not milliseconds switch (unit) { - case 'minutes': return 0.001 * amount * DateUtils.MINUTE - case 'hours': return 0.001 * amount * DateUtils.HOUR - case 'days': return 0.001 * amount * DateUtils.DAY + case 'minutes': return amount * DateUtils.MINUTE / 1000 + case 'hours': return amount * DateUtils.HOUR / 1000 + case 'days': return amount * DateUtils.DAY / 1000 } }, expiryAmountChange () { diff --git a/src/components/post_status_form/post_status_form.js b/src/components/post_status_form/post_status_form.js index b8539395..8558d45d 100644 --- a/src/components/post_status_form/post_status_form.js +++ b/src/components/post_status_form/post_status_form.js @@ -54,6 +54,14 @@ const pxStringToNumber = (str) => { return Number(str.substring(0, str.length - 2)) } +const deleteDraft = (draftKey) => { + const draftData = JSON.parse(localStorage.getItem('drafts') || '{}'); + + delete draftData[draftKey]; + + localStorage.setItem('drafts', JSON.stringify(draftData)); +} + const PostStatusForm = { props: [ 'statusId', @@ -161,6 +169,36 @@ const PostStatusForm = { } } + if (!this.statusId) { + let draftKey = 'status'; + if (this.replyTo) { + draftKey = 'reply:' + this.replyTo; + } else if (this.quoteId) { + draftKey = 'quote:' + this.quoteId; + } + + const draft = JSON.parse(localStorage.getItem('drafts') || '{}')[draftKey]; + + if (draft) { + statusParams = { + spoilerText: draft.data.spoilerText, + status: draft.data.status, + sensitiveIfSubject, + nsfw: draft.data.nsfw, + files: draft.data.files, + poll: draft.data.poll, + mediaDescriptions: draft.data.mediaDescriptions, + visibility: draft.data.visibility, + language: draft.data.language, + contentType: draft.data.contentType + } + + if (draft.data.poll) { + this.togglePollForm(); + } + } + } + return { dropFiles: [], uploadingFiles: false, @@ -280,6 +318,7 @@ const PostStatusForm = { statusChanged () { this.autoPreview() this.updateIdempotencyKey() + this.saveDraft() }, clearStatus () { const newStatus = this.newStatus @@ -401,8 +440,38 @@ const PostStatusForm = { }).finally(() => { this.previewLoading = false }) + + let draftKey = 'status'; + if (this.replyTo) { + draftKey = 'reply:' + this.replyTo; + } else if (this.quoteId) { + draftKey = 'quote:' + this.quoteId; + } + deleteDraft(draftKey) }, debouncePreviewStatus: debounce(function () { this.previewStatus() }, 500), + saveDraft() { + const draftData = JSON.parse(localStorage.getItem('drafts') || '{}'); + + let draftKey = 'status'; + if (this.replyTo) { + draftKey = 'reply:' + this.replyTo; + } else if (this.quoteId) { + draftKey = 'quote:' + this.quoteId; + } + + if (this.newStatus.status || this.newStatus.spoilerText || this.newStatus.files.length > 0 || this.newStatus.poll.length > 0) { + draftData[draftKey] = { + updatedAt: new Date(), + data: this.newStatus, + }; + + localStorage.setItem('drafts', JSON.stringify(draftData)); + + } else { + deleteDraft(draftKey); + } + }, autoPreview () { if (!this.preview) return this.previewLoading = true diff --git a/src/components/rich_content/rich_content.jsx b/src/components/rich_content/rich_content.jsx index c5fb1688..0da7f3de 100644 --- a/src/components/rich_content/rich_content.jsx +++ b/src/components/rich_content/rich_content.jsx @@ -188,7 +188,7 @@ export default { break } case 'span': - if (this.handleLinks && attrs['class'] && attrs['class'].includes('h-card')) { + if (this.handleLinks && attrs?.['class']?.includes?.('h-card')) { return ['', children.map(processItem), ''] } } diff --git a/src/components/rich_content/rich_content.scss b/src/components/rich_content/rich_content.scss index db08ef1e..63df7d74 100644 --- a/src/components/rich_content/rich_content.scss +++ b/src/components/rich_content/rich_content.scss @@ -50,7 +50,6 @@ .emoji { display: inline-block; - width: var(--emoji-size, 32px); height: var(--emoji-size, 32px); } diff --git a/src/components/status_body/status_body.scss b/src/components/status_body/status_body.scss index 230a27ac..d618f65e 100644 --- a/src/components/status_body/status_body.scss +++ b/src/components/status_body/status_body.scss @@ -22,21 +22,18 @@ ._mfm_x2_ { .emoji { - width: 100px; height: 100px; } } ._mfm_x3_ { .emoji { - width: 150px; height: 150px; } } ._mfm_x4_ { .emoji { - width: 200px; height: 200px; } } diff --git a/src/components/status_content/status_content.vue b/src/components/status_content/status_content.vue index ab13141e..62acf5ac 100644 --- a/src/components/status_content/status_content.vue +++ b/src/components/status_content/status_content.vue @@ -71,7 +71,7 @@ img, video { &.emoji { - width: 50px; + max-width: 100%; height: 50px; } } @@ -89,7 +89,6 @@ animation: none !important; } .emoji { - width: 32px !important; height: 32px !important; } } diff --git a/src/components/timeline_menu/timeline_menu_content.js b/src/components/timeline_menu/timeline_menu_content.js index df15030b..80cf6937 100644 --- a/src/components/timeline_menu/timeline_menu_content.js +++ b/src/components/timeline_menu/timeline_menu_content.js @@ -8,6 +8,7 @@ import { faHome, faCircle } from '@fortawesome/free-solid-svg-icons' +import { federatedTimelineVisible, publicTimelineVisible, bubbleTimelineVisible } from '../../lib/timeline_visibility' library.add( faUsers, @@ -24,7 +25,9 @@ const TimelineMenuContent = { currentUser: state => state.users.currentUser, privateMode: state => state.instance.private, federating: state => state.instance.federating, - showBubbleTimeline: state => (state.instance.localBubbleInstances.length > 0) + publicTimelineVisible, + federatedTimelineVisible, + bubbleTimelineVisible, }) } } diff --git a/src/components/timeline_menu/timeline_menu_content.vue b/src/components/timeline_menu/timeline_menu_content.vue index 27aece22..bb170b82 100644 --- a/src/components/timeline_menu/timeline_menu_content.vue +++ b/src/components/timeline_menu/timeline_menu_content.vue @@ -16,7 +16,7 @@ >{{ $t("nav.home_timeline") }} -
  • +
  • {{ $t("nav.bubble_timeline") }}
  • -
  • +
  • {{ $t("nav.public_tl") }}
  • -
  • +
  • {{ $t("nav.twkn") }} +
  • diff --git a/src/components/timeline_menu_tabs/timeline_menu_content.js b/src/components/timeline_menu_tabs/timeline_menu_content.js index 9c2ef0c9..00df2120 100644 --- a/src/components/timeline_menu_tabs/timeline_menu_content.js +++ b/src/components/timeline_menu_tabs/timeline_menu_content.js @@ -8,6 +8,7 @@ import { faHome } from '@fortawesome/free-solid-svg-icons' import { faCircle } from '@fortawesome/free-regular-svg-icons' +import { federatedTimelineVisible, publicTimelineVisible, bubbleTimelineVisible } from '../../lib/timeline_visibility' library.add( faUsers, faGlobe, @@ -22,7 +23,10 @@ const TimelineMenuContent = { ...mapState({ currentUser: state => state.users.currentUser, privateMode: state => state.instance.private, - federating: state => state.instance.federating + federating: state => state.instance.federating, + publicTimelineVisible, + federatedTimelineVisible, + bubbleTimelineVisible, }) } } diff --git a/src/components/timeline_menu_tabs/timeline_menu_content.vue b/src/components/timeline_menu_tabs/timeline_menu_content.vue index 32548c49..28e58714 100644 --- a/src/components/timeline_menu_tabs/timeline_menu_content.vue +++ b/src/components/timeline_menu_tabs/timeline_menu_content.vue @@ -16,7 +16,7 @@ >{{ $t("nav.home_timeline") }}
  • -
  • +
  • {{ $t("nav.bubble_timeline") }}
  • -
  • +
  • {{ $t("nav.public_tl") }}
  • -
  • +
  • state.users.currentUser, + publicTimelineVisible, + federatedTimelineVisible, + bubbleTimelineVisible, + }) }, methods: { timelineName () { diff --git a/src/components/timeline_menu_tabs/timeline_menu_tabs.vue b/src/components/timeline_menu_tabs/timeline_menu_tabs.vue index f017fa1f..96340508 100644 --- a/src/components/timeline_menu_tabs/timeline_menu_tabs.vue +++ b/src/components/timeline_menu_tabs/timeline_menu_tabs.vue @@ -18,6 +18,7 @@ @@ -41,6 +42,7 @@ 0 会同设置成零一样" }, - "hintV3": "对于阴影,您还可以使用 {0} 表示法来使用其它颜色插槽。", + "hintV3": "对于阴影,你还可以使用 {0} 表示法来使用其它颜色插槽。", "inset": "内阴影", "override": "覆盖", "shadow_id": "阴影 #{value}", @@ -614,24 +871,24 @@ }, "switcher": { "clear_all": "清除全部", - "clear_opacity": "清除透明度", + "clear_opacity": "清除不透明度", "help": { - "fe_downgraded": "PleromaFE 的版本回滚了。", - "fe_upgraded": "PleromaFE 的主题引擎随着版本更新升级了。", - "future_version_imported": "您导入的文件来自更高版本的 FE。", - "migration_napshot_gone": "不知出于何种原因,主题快照缺失了,一些地方可能与您印象中的不符。", - "migration_snapshot_ok": "为保万无一失,加载了主题快照。您可以试着加载主题数据。", - "older_version_imported": "您导入的文件来自旧版本的 FE。", + "fe_downgraded": "PleromaFE 的版本已回滚。", + "fe_upgraded": "PleromaFE 的主题引擎已在版本更新后升级。", + "future_version_imported": "你所导入的文件配置于更高版本的 FE。", + "migration_napshot_gone": "不知出于何种原因,主题快照缺失了,一些地方可能看起来与你记忆中的不太一样。", + "migration_snapshot_ok": "为保万无一失,主题快照已加载。你可以试着加载主题数据。", + "older_version_imported": "你所导入的文件配置于旧版 FE。", "snapshot_missing": "在文件中没有主题快照,所以网站外观可能会与原来预想的不同。", - "snapshot_present": "主题快照已加载,因此所有的值均被覆盖。您可以改为加载主题的实际数据。", - "snapshot_source_mismatch": "版本冲突:很有可能是 FE 版本回滚后再次升级了,如果您使用旧版本的 FE 更改了主题那么您可能需要使用旧版本,否则请使用新版本。", - "upgraded_from_v2": "PleromaFE 已升级,主题会与您记忆中的不太一样。", - "v2_imported": "您导入的文件是旧版 FE 的。我们尽可能保持兼容性,但还是可能出现不一致的情况。" + "snapshot_present": "主题快照已加载,因此所有的值均被覆盖。你可以改为加载主题的实际数据。", + "snapshot_source_mismatch": "版本冲突:很有可能是 FE 版本回滚后再次升级了,如果你使用旧版本的 FE 更改了主题那么你可能需要使用旧版本,否则请使用新版本。", + "upgraded_from_v2": "PleromaFE 已升级,主题可能看起来与你记忆中的不太一样。", + "v2_imported": "你所导入的文件配置于旧版 FE。我们尽可能提高兼容性,但还是可能出现不一致的情况。" }, "keep_as_is": "保持原状", "keep_color": "保留颜色", "keep_fonts": "保留字体", - "keep_opacity": "保留透明度", + "keep_opacity": "保留不透明度", "keep_roundness": "保留圆角", "keep_shadows": "保留阴影", "load_theme": "加载主题", @@ -641,27 +898,38 @@ "use_source": "新版本" } }, - "subject_input_always_show": "总是显示主题框", - "subject_line_behavior": "回复时复制主题", - "subject_line_email": "类似电子邮件: \"re: 主题\"", + "subject_input_always_show": "始终显示内容警告字段", + "subject_line_behavior": "回复时复制内容警告", + "subject_line_email": "类似电子邮件:\"re: 警告\"", "subject_line_mastodon": "类似 mastodon: 与原主题相同", "subject_line_noop": "不要复制", "text": "文本", "theme": "主题", "theme_help": "使用十六进制代码(#rrggbb)来设置主题颜色。", - "theme_help_v2_1": "您也可以通过选中复选框来覆盖某些组件的颜色和透明度。使用“清除所有”按钮来清除所有覆盖设置。", - "theme_help_v2_2": "某些条目下的图标是背景或文本对比指示器,鼠标悬停可以获取详细信息。请记住,使用透明度来显示最差的情况。", + "theme_help_v2_1": "你也可以通过选中复选框来覆盖某些组件的颜色和不透明度。使用“清除所有”按钮来清除所有覆盖设置。", + "theme_help_v2_2": "某些条目下的图标是背景或文本对比度指示器,悬停在上方以获取详细信息。请记住,当使用透明度对比指标时,显示的是最坏的情况。", + "third_column_mode": "当有足够的空间时,显示第三栏包含", + "third_column_mode_none": "完全不显示第三栏", + "third_column_mode_notifications": "通知栏", + "third_column_mode_postform": "主要的发布窗口和导航", "token": "令牌", "tooltipRadius": "提醒", - "type_domains_to_mute": "搜索需要隐藏的域名", + "translation_language": "自动翻译语言", + "tree_advanced": "显示额外的按钮来打开和关闭同主题帖文中的回复链", + "tree_fade_ancestors": "在当前帖子中以淡色文本显示其原型帖子", + "type_domains_to_mute": "搜索需要静音的域名", "upload_a_photo": "上传照片", - "useStreamingApi": "实时接收帖子和通知", - "useStreamingApiWarning": "(不推荐使用,试验性,已知会跳过一些帖子)", + "useStreamingApi": "实时接收帖文和通知", + "useStreamingApiWarning": "(不推荐使用,试验性,已知会跳过一些帖文)", + "use_at_icon": "将 {'@'} 符号显示为图标而不是文本", + "use_blurhash": "对NSFW的缩略图使用模糊处理", "use_contain_fit": "生成缩略图时不要裁剪附件", "use_one_click_nsfw": "点击一次以打开工作场所不适宜(NSFW)的附件", "user_mutes": "用户", + "user_profile_default_tab": "用户资料中的默认标签页", + "user_profiles": "用户资料", "user_settings": "用户设置", - "valid_until": "有效期至", + "valid_until": "有效期到", "values": { "false": "否", "true": "是" @@ -672,39 +940,91 @@ "title": "版本" }, "virtual_scrolling": "优化时间线渲染", - "word_filter": "词语过滤" + "word_filter": "词语过滤", + "wordfilter": "词语过滤器" + }, + "settings_profile": { + "creating": "创建新的设置配置文件 \"{profile}\" 中...", + "synchronization_error": "无法同步设置:{err}", + "synchronized": "设置已同步!", + "synchronizing": "同步新的设置配置文件 \"{profile}\" 中..." }, "status": { + "ancestor_follow": "查看此帖文下其它 {numReplies} 条回复", + "ancestor_follow_with_icon": "{icon} {text}", + "attachment_stop_flash": "停止 Flash 播放器", "bookmark": "书签", - "copy_link": "复制状态链接", - "delete": "删除状态", - "delete_confirm": "你真的想要删除这条状态吗?", + "collapse_attachments": "折起附件", + "copy_link": "复制帖文链接", + "delete": "删除帖文", + "delete_confirm": "你真的想要删除此帖文吗?", + "delete_confirm_accept_button": "是,删除它", + "delete_confirm_cancel_button": "否,保留它", + "delete_confirm_title": "确认删除", + "edit": "编辑", + "edit_history": "编辑历史", + "edit_history_modal_title": "已编辑 {historyCount} 次", + "edited_at": "编辑于 {time}", "expand": "展开", "external_source": "外部来源", "favorites": "喜欢", + "hide_attachment": "隐藏附件", "hide_content": "隐藏内容", - "hide_full_subject": "隐藏此部分标题", - "mute_conversation": "隐藏对话", + "hide_full_subject": "隐藏全部内容警告", + "many_attachments": "帖文有 {number} 个附件", + "mentions": "提及", + "move_down": "把附件右移", + "move_up": "把附件左移", + "mute_conversation": "静音对话", "nsfw": "NSFW", + "open_gallery": "打开图库", + "override_translation_source_language": "覆盖源语言", "pin": "在个人资料置顶", "pinned": "置顶", + "plus_more": "还有 {number} 个", + "redraft": "删除并改写", + "redraft_confirm": "你确定要删除并改写此帖文吗?与原帖文的互动将不会被保留。", + "redraft_confirm_accept_button": "是,删除并改写", + "redraft_confirm_cancel_button": "否,保持原样", + "redraft_confirm_title": "确认删除并改写", + "remove_attachment": "移除附件", + "repeat_confirm": "你确定要转发此帖文吗?", + "repeat_confirm_accept_button": "是,转发它", + "repeat_confirm_cancel_button": "否,不要转发", + "repeat_confirm_title": "确认转发", "repeats": "转发", "replies_list": "回复:", + "replies_list_with_others": "查看 {numReplies} 条更多回复", "reply_to": "回复", + "show_all_attachments": "显示所有附件", + "show_all_conversation": "显示完整对话(其它 {numStatus} 条帖文)", + "show_all_conversation_with_icon": "{icon} {text}", + "show_attachment_description": "预览描述(打开附件能看完整描述)", + "show_attachment_in_modal": "在窗口中显示附件", "show_content": "显示内容", - "show_full_subject": "显示全部标题", - "status_deleted": "该状态已被删除", - "status_unavailable": "状态不可取得", - "thread_muted": "此系列消息已被隐藏", + "show_full_subject": "显示全部内容警告", + "show_only_conversation_under_this": "仅显示此帖文的回复", + "status_deleted": "此帖文已被删除", + "status_unavailable": "帖文不可用", + "thread_follow": "查看 {numStatus} 条更多回复", + "thread_follow_with_icon": "{icon} {text}", + "thread_hide": "隐藏此同主题帖文", + "thread_muted": "同主题帖文已被静音", "thread_muted_and_words": ",含有过滤词:", + "thread_show": "显示这个同主题帖文", + "thread_show_full": "显示 {numStatus} 条回复 | 显示全部 {numStatus} 条回复", + "thread_show_full_with_icon": "{icon} {text}", + "translate": "翻译", + "translated_from": "翻译自 {language}", "unbookmark": "取消书签", - "unmute_conversation": "对话取消隐藏", - "unpin": "取消在个人资料置顶" + "unmute_conversation": "对话取消静音", + "unpin": "取消在个人资料置顶", + "you": "(你)" }, "time": { - "in_future": "还有 {0}", + "in_future": "{0} 之后", "in_past": "{0} 之前", - "now": "刚刚", + "now": "刚才", "now_short": "刚刚", "unit": { "days": "{0} 天", @@ -726,24 +1046,30 @@ "timeline": { "collapse": "折叠", "conversation": "对话", - "error": "取得时间轴时发生错误:{0}", - "load_older": "加载更早的状态", - "no_more_statuses": "没有更多的状态", - "no_retweet_hint": "这条内容仅关注者可见,或者是私信,因此不能转发", - "no_statuses": "没有状态更新", + "error": "获取时间线时发生错误:{0}", + "follow_tag": "关注话题标签", + "load_older": "加载更早的帖文", + "no_more_statuses": "没有更多的帖文", + "no_retweet_hint": "帖文仅关注者可见,或者是私信,因此不能转发或引用", + "no_statuses": "没有帖文更新", "reload": "重新载入", - "repeated": "转发了", - "show_new": "显示新内容", + "repeated": "已转发", + "show_new": "显示新帖文", "socket_broke": "丢失实时连接:CloseEvent code {0}", "socket_reconnected": "已建立实时连接", + "unfollow_tag": "取消关注话题标签", "up_to_date": "已是最新" }, + "toast": { + "no_translation_target_set": "没有设置翻译目标语言 - 这可能会失败。请在你的设置中设置目标语言。" + }, "tool_tip": { "accept_follow_request": "接受关注请求", "add_reaction": "添加互动", "bookmark": "书签", "favorite": "喜欢", "media_upload": "上传媒体", + "quote": "引用", "reject_follow_request": "拒绝关注请求", "repeat": "转发", "reply": "回复", @@ -751,8 +1077,8 @@ }, "upload": { "error": { - "base": "上传不成功。", - "default": "迟些再试", + "base": "上传失败。", + "default": "稍后再试", "file_too_big": "文件太大了 [{filesize}{filesizeunit} / {allowedsize}{allowedsizeunit}]", "message": "上传错误:{0}" }, @@ -770,31 +1096,51 @@ "deactivate_account": "关闭账号", "delete_account": "删除账号", "delete_user": "删除用户", + "delete_user_data_and_deactivate_confirmation": "这将永久删除该账号的数据并停用该账号。你完全确定吗?", "disable_any_subscription": "完全禁止关注用户", "disable_remote_subscription": "禁止从远程实例关注用户", - "force_nsfw": "标记所有的帖子都是 - 工作场合不适", - "force_unlisted": "强制帖子为不公开", - "grant_admin": "赋予管理权限", + "force_nsfw": "标记所有帖文为NSFW", + "force_unlisted": "强制帖文设为不公开", + "grant_admin": "赋予管理员权限", "grant_moderator": "赋予监察员权限", "moderation": "仲裁", - "quarantine": "从联合实例中禁止用户帖子", + "quarantine": "禁止用户贴文发送至其它实例", "revoke_admin": "撤销管理权限", "revoke_moderator": "撤销监察员权限", - "sandbox": "强制帖子为只有关注者可看", - "strip_media": "从帖子里删除媒体文件" + "sandbox": "强制帖文设为仅关注者", + "strip_media": "从帖文里删除媒体文件" }, - "approve": "核准", + "approve": "批准", + "approve_confirm": "你确定要让此用户关注你吗?", + "approve_confirm_accept_button": "是,接受", + "approve_confirm_cancel_button": "否,取消", + "approve_confirm_title": "通过关注请求", "block": "屏蔽", - "block_progress": "正在屏蔽…", + "block_confirm": "你确定要屏蔽 {user} 吗?", + "block_confirm_accept_button": "是,屏蔽", + "block_confirm_cancel_button": "否,不要屏蔽", + "block_confirm_title": "屏蔽用户", + "block_progress": "屏蔽中…", "blocked": "已屏蔽!", + "blocks_you": "屏蔽了你!", "bot": "机器人", + "deactivated": "已停用", "deny": "拒绝", + "deny_confirm": "你确定要拒绝此用户的关注请求吗?", + "deny_confirm_accept_button": "是,拒绝", + "deny_confirm_cancel_button": "否,取消", + "deny_confirm_title": "拒绝关注请求", + "domain_muted": "取消屏蔽域名", "edit_profile": "编辑个人资料", "favorites": "喜欢", "follow": "关注", + "follow_cancel": "取消请求", "follow_progress": "请求中…", "follow_sent": "请求已发送!", + "follow_tag": "关注话题标签", "follow_unfollow": "取消关注", + "followed_tags": "已关注话题标签", + "followed_users": "已关注用户", "followees": "正在关注", "followers": "关注者", "following": "正在关注!", @@ -811,37 +1157,53 @@ "media": "媒体", "mention": "提及", "message": "消息", - "mute": "隐藏", - "mute_progress": "隐藏中…", - "muted": "已隐藏", + "mute": "静音", + "mute_confirm": "你确定要静音 {user} 吗?", + "mute_confirm_accept_button": "是,静音", + "mute_confirm_cancel_button": "否,不要静音", + "mute_confirm_title": "静音用户", + "mute_domain": "屏蔽域名", + "mute_progress": "静音中…", + "muted": "已静音", + "not_following_any_hashtags": "你没有关注任何话题标签", + "note": "私有备注", "per_day": "每天", "remote_follow": "跨站关注", - "report": "报告", + "remove_follower": "移除关注者", + "replies": "和回复", + "report": "举报", + "requested_by": "已请求关注你", "show_repeats": "显示转发", - "statuses": "状态", + "statuses": "帖文", "subscribe": "订阅", "unblock": "取消屏蔽", - "unblock_progress": "正在取消屏蔽…", - "unmute": "取消隐藏", - "unmute_progress": "取消隐藏中…", + "unblock_progress": "取消屏蔽中…", + "unfollow_confirm": "你确定要取消关注 {user} 吗?", + "unfollow_confirm_accept_button": "是,取消关注", + "unfollow_confirm_cancel_button": "否,不要取消关注", + "unfollow_confirm_title": "取消关注用户", + "unfollow_tag": "取消关注话题标签", + "unmute": "取消静音", + "unmute_progress": "取消静音中…", "unsubscribe": "退订" }, "user_profile": { + "field_validated": "链接已验证", "profile_does_not_exist": "抱歉,此个人资料不存在。", "profile_loading_error": "抱歉,载入个人资料时出错。", "timeline_title": "用户时间线" }, "user_reporting": { - "add_comment_description": "此报告会发送给您的实例监察员。您可以在下面提供更多详细信息解释报告的缘由:", - "additional_comments": "其它信息", - "forward_description": "这个账号来自另一个服务器。是否同时发送一份报告副本到那里?", - "forward_to": "转发 {0}", - "generic_error": "当处理您的请求时,发生了一个错误。", + "add_comment_description": "此举报会发送给你所在实例的监察员。你可以在下面提供更多详细信息解释举报的缘由:", + "additional_comments": "补充意见", + "forward_description": "此账号来自另一个服务器。是否也发送一份举报的副本到那里?", + "forward_to": "转发到 {0}", + "generic_error": "当处理你的请求时,发生了一个错误。", "submit": "提交", - "title": "报告 {0}" + "title": "举报 {0} 中" }, "who_to_follow": { "more": "更多", "who_to_follow": "推荐关注" } -} \ No newline at end of file +} diff --git a/src/lib/persisted_state.js b/src/lib/persisted_state.js index 735a10de..a9a9d307 100644 --- a/src/lib/persisted_state.js +++ b/src/lib/persisted_state.js @@ -19,7 +19,8 @@ const saveImmedeatelyActions = [ 'setOption', 'setClientData', 'setToken', - 'clearToken' + 'clearToken', + 'emojiUsed', ] const defaultStorage = (() => { diff --git a/src/lib/timeline_visibility.js b/src/lib/timeline_visibility.js new file mode 100644 index 00000000..379f6214 --- /dev/null +++ b/src/lib/timeline_visibility.js @@ -0,0 +1,23 @@ +const timelineVisibleUnauthenticated = (state, timeline) => ( + state.instance.publicTimelineVisibility[timeline] ?? false +); + +const currentUser = (state) => state.users.currentUser; + +const currentUserOrTimelineVisibleUnauthenticated = (state, timeline) => ( + currentUser(state) || timelineVisibleUnauthenticated(state, timeline) +); + +const federatedTimelineAvailable = (state) => state.instance.federatedTimelineAvailable; + +export const federatedTimelineVisible = (state) => ( + federatedTimelineAvailable(state) && currentUserOrTimelineVisibleUnauthenticated(state, 'federated') +); + +export const publicTimelineVisible = (state) => ( + currentUserOrTimelineVisibleUnauthenticated(state, 'local') +); + +export const bubbleTimelineVisible = (state) => ( + state.instance.localBubbleInstances.length > 0 && currentUserOrTimelineVisibleUnauthenticated(state, 'bubble') +); diff --git a/src/main.js b/src/main.js index 01a44c96..cad887e5 100644 --- a/src/main.js +++ b/src/main.js @@ -22,6 +22,7 @@ import announcementsModule from './modules/announcements.js' import editStatusModule from './modules/editStatus.js' import statusHistoryModule from './modules/statusHistory.js' import tagModule from './modules/tags.js' +import recentEmojisModule from './modules/recentEmojis.js' import { createI18n } from 'vue-i18n' @@ -47,7 +48,8 @@ const persistedStateOptions = { paths: [ 'config', 'users.lastLoginName', - 'oauth' + 'oauth', + 'recentEmojis.emojis', ] }; @@ -98,7 +100,8 @@ const persistedStateOptions = { announcements: announcementsModule, editStatus: editStatusModule, statusHistory: statusHistoryModule, - tags: tagModule + tags: tagModule, + recentEmojis: recentEmojisModule, }, plugins, strict: false // Socket modifies itself, let's ignore this for now. diff --git a/src/modules/api.js b/src/modules/api.js index 8de1449b..352d7774 100644 --- a/src/modules/api.js +++ b/src/modules/api.js @@ -5,15 +5,25 @@ import { map } from 'lodash' const retryTimeout = (multiplier) => 1000 * multiplier const isVisible = (store, message, visibility) => { - if (visibility === 'all') { + if (visibility == 'all') { return true - } else if (visibility === 'following') { - return store.getters.relationship(message.in_reply_to_user_id).following - } else if (visibility === 'self') { + } + + if (visibility == 'following') { + if (message.in_reply_to_user_id === null) { + return true + } else { + return store.getters.relationship(message.in_reply_to_user_id).following + } + } + + if (visibility == 'self') { return message.in_reply_to_user_id === store.rootState.users.currentUser.id } + return false } + const api = { state: { retryMultiplier: 1, diff --git a/src/modules/recentEmojis.js b/src/modules/recentEmojis.js new file mode 100644 index 00000000..baab1c52 --- /dev/null +++ b/src/modules/recentEmojis.js @@ -0,0 +1,50 @@ +// each row is 7 emojis, 6 rows chosen arbitrarily. i don't think more than +// that are going to be useful. +const RECENT_MAX = 7 * 6 + +const defaultState = { + emojis: [], +} + +const recentEmojis = { + state: defaultState, + + mutations: { + emojiUsed ({ emojis }, emoji) { + if (emoji.displayText === undefined || emoji.displayText === null) { + console.error('emojiUsed was called with a bad emoji object: ', emoji) + return + } else if (emoji.displayText.includes('@')) { + console.error('emojiUsed was called with a remote emoji: ', emoji) + return + } + + const i = emojis.indexOf(emoji.displayText) + + if (i === -1) { + // not in `emojis` yet, insert and truncate if necessary + const newLength = emojis.unshift(emoji.displayText) + if (newLength > RECENT_MAX) { + emojis.pop() + } + } else if (i !== 0) { + // emoji is already in `emojis` but needs to be bumped to the top + emojis.splice(i, 1) + emojis.unshift(emoji.displayText) + } + }, + }, + + getters: { + recentEmojis: (state, getters, rootState) => state.emojis.reduce((objects, displayText) => { + const allEmojis = rootState.instance.emoji.concat(rootState.instance.customEmoji) + let emojiObject = allEmojis.find(emoji => emoji.displayText === displayText) + if (emojiObject !== undefined) { + objects.push(emojiObject) + } + return objects + }, []), + }, +} + +export default recentEmojis diff --git a/src/modules/users.js b/src/modules/users.js index bc1943c8..9d81f9bc 100644 --- a/src/modules/users.js +++ b/src/modules/users.js @@ -637,13 +637,16 @@ const users = { // Get user mutes store.dispatch('fetchMutes') - store.dispatch('setLayoutWidth', windowWidth()) store.dispatch('setLayoutHeight', windowHeight()) store.dispatch('getSupportedTranslationlanguages') store.dispatch('getSettingsProfile') store.dispatch('listSettingsProfiles') store.dispatch('startFetchingConfig') + store.dispatch('startFetchingAnnouncements') + if (user.role === 'admin' || user.role === 'moderator') { + store.dispatch('startFetchingReports') + } // Fetch our friends store.rootState.api.backendInteractor.fetchFriends({ id: user.id })