diff options
Diffstat (limited to 'src')
40 files changed, 913 insertions, 71 deletions
diff --git a/src/App.vue b/src/App.vue index 23a388a6..fe214ce7 100644 --- a/src/App.vue +++ b/src/App.vue @@ -71,7 +71,6 @@ <StatusHistoryModal v-if="editingAvailable" /> <SettingsModal /> <UpdateNotification /> - <div id="modal" /> <GlobalNoticeList /> </div> </template> diff --git a/src/components/account_actions/account_actions.js b/src/components/account_actions/account_actions.js index c23407f9..acd93e06 100644 --- a/src/components/account_actions/account_actions.js +++ b/src/components/account_actions/account_actions.js @@ -2,6 +2,7 @@ import { mapState } from 'vuex' import ProgressButton from '../progress_button/progress_button.vue' import Popover from '../popover/popover.vue' import UserListMenu from 'src/components/user_list_menu/user_list_menu.vue' +import ConfirmModal from '../confirm_modal/confirm_modal.vue' import { library } from '@fortawesome/fontawesome-svg-core' import { faEllipsisV @@ -16,14 +17,30 @@ const AccountActions = { 'user', 'relationship' ], data () { - return { } + return { + showingConfirmBlock: false, + showingConfirmRemoveFollower: false + } }, components: { ProgressButton, Popover, - UserListMenu + UserListMenu, + ConfirmModal }, methods: { + showConfirmBlock () { + this.showingConfirmBlock = true + }, + hideConfirmBlock () { + this.showingConfirmBlock = false + }, + showConfirmRemoveUserFromFollowers () { + this.showingConfirmRemoveFollower = true + }, + hideConfirmRemoveUserFromFollowers () { + this.showingConfirmRemoveFollower = false + }, showRepeats () { this.$store.dispatch('showReblogs', this.user.id) }, @@ -31,13 +48,29 @@ const AccountActions = { this.$store.dispatch('hideReblogs', this.user.id) }, blockUser () { + if (!this.shouldConfirmBlock) { + this.doBlockUser() + } else { + this.showConfirmBlock() + } + }, + doBlockUser () { this.$store.dispatch('blockUser', this.user.id) + this.hideConfirmBlock() }, unblockUser () { this.$store.dispatch('unblockUser', this.user.id) }, removeUserFromFollowers () { + if (!this.shouldConfirmRemoveUserFromFollowers) { + this.doRemoveUserFromFollowers() + } else { + this.showConfirmRemoveUserFromFollowers() + } + }, + doRemoveUserFromFollowers () { this.$store.dispatch('removeUserFromFollowers', this.user.id) + this.hideConfirmRemoveUserFromFollowers() }, reportUser () { this.$store.dispatch('openUserReportingModal', { userId: this.user.id }) @@ -50,6 +83,12 @@ const AccountActions = { } }, computed: { + shouldConfirmBlock () { + return this.$store.getters.mergedConfig.modalOnBlock + }, + shouldConfirmRemoveUserFromFollowers () { + return this.$store.getters.mergedConfig.modalOnRemoveUserFromFollowers + }, ...mapState({ pleromaChatMessagesAvailable: state => state.instance.pleromaChatMessagesAvailable }) diff --git a/src/components/account_actions/account_actions.vue b/src/components/account_actions/account_actions.vue index 973a5935..ce19291a 100644 --- a/src/components/account_actions/account_actions.vue +++ b/src/components/account_actions/account_actions.vue @@ -74,6 +74,48 @@ </button> </template> </Popover> + <teleport to="#modal"> + <confirm-modal + v-if="showingConfirmBlock" + :title="$t('user_card.block_confirm_title')" + :confirm-text="$t('user_card.block_confirm_accept_button')" + :cancel-text="$t('user_card.block_confirm_cancel_button')" + @accepted="doBlockUser" + @cancelled="hideConfirmBlock" + > + <i18n-t + keypath="user_card.block_confirm" + tag="span" + > + <template #user> + <span + v-text="user.screen_name_ui" + /> + </template> + </i18n-t> + </confirm-modal> + </teleport> + <teleport to="#modal"> + <confirm-modal + v-if="showingConfirmRemoveFollower" + :title="$t('user_card.remove_follower_confirm_title')" + :confirm-text="$t('user_card.remove_follower_confirm_accept_button')" + :cancel-text="$t('user_card.remove_follower_confirm_cancel_button')" + @accepted="doRemoveUserFromFollowers" + @cancelled="hideConfirmRemoveUserFromFollowers" + > + <i18n-t + keypath="user_card.remove_follower_confirm" + tag="span" + > + <template #user> + <span + v-text="user.screen_name_ui" + /> + </template> + </i18n-t> + </confirm-modal> + </teleport> </div> </template> diff --git a/src/components/confirm_modal/confirm_modal.js b/src/components/confirm_modal/confirm_modal.js new file mode 100644 index 00000000..96ddc118 --- /dev/null +++ b/src/components/confirm_modal/confirm_modal.js @@ -0,0 +1,37 @@ +import DialogModal from '../dialog_modal/dialog_modal.vue' + +/** + * This component emits the following events: + * cancelled, emitted when the action should not be performed; + * accepted, emitted when the action should be performed; + * + * The caller should close this dialog after receiving any of the two events. + */ +const ConfirmModal = { + components: { + DialogModal + }, + props: { + title: { + type: String + }, + cancelText: { + type: String + }, + confirmText: { + type: String + } + }, + computed: { + }, + methods: { + onCancel () { + this.$emit('cancelled') + }, + onAccept () { + this.$emit('accepted') + } + } +} + +export default ConfirmModal diff --git a/src/components/confirm_modal/confirm_modal.vue b/src/components/confirm_modal/confirm_modal.vue new file mode 100644 index 00000000..3b98174a --- /dev/null +++ b/src/components/confirm_modal/confirm_modal.vue @@ -0,0 +1,29 @@ +<template> + <dialog-modal + v-body-scroll-lock="true" + class="confirm-modal" + :on-cancel="onCancel" + > + <template #header> + <span v-text="title" /> + </template> + + <slot /> + + <template #footer> + <button + class="btn button-default" + @click.prevent="onAccept" + v-text="confirmText" + /> + + <button + class="btn button-default" + @click.prevent="onCancel" + v-text="cancelText" + /> + </template> + </dialog-modal> +</template> + +<script src="./confirm_modal.js"></script> diff --git a/src/components/desktop_nav/desktop_nav.js b/src/components/desktop_nav/desktop_nav.js index 08c0e44e..745b1a81 100644 --- a/src/components/desktop_nav/desktop_nav.js +++ b/src/components/desktop_nav/desktop_nav.js @@ -1,4 +1,5 @@ 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 { faSignInAlt, @@ -30,7 +31,8 @@ library.add( export default { components: { - SearchBar + SearchBar, + ConfirmModal }, data: () => ({ searchBarHidden: true, @@ -40,7 +42,8 @@ export default { window.CSS.supports('-moz-mask-size', 'contain') || window.CSS.supports('-ms-mask-size', 'contain') || window.CSS.supports('-o-mask-size', 'contain') - ) + ), + showingConfirmLogout: false }), computed: { enableMask () { return this.supportsMask && this.$store.state.instance.logoMask }, @@ -73,15 +76,32 @@ export default { hideSitename () { return this.$store.state.instance.hideSitename }, logoLeft () { return this.$store.state.instance.logoLeft }, currentUser () { return this.$store.state.users.currentUser }, - privateMode () { return this.$store.state.instance.private } + privateMode () { return this.$store.state.instance.private }, + shouldConfirmLogout () { + return this.$store.getters.mergedConfig.modalOnLogout + } }, methods: { scrollToTop () { window.scrollTo(0, 0) }, + showConfirmLogout () { + this.showingConfirmLogout = true + }, + hideConfirmLogout () { + this.showingConfirmLogout = false + }, logout () { + if (!this.shouldConfirmLogout) { + this.doLogout() + } else { + this.showConfirmLogout() + } + }, + doLogout () { this.$router.replace('/main/public') this.$store.dispatch('logout') + this.hideConfirmLogout() }, onSearchBarToggled (hidden) { this.searchBarHidden = hidden diff --git a/src/components/desktop_nav/desktop_nav.vue b/src/components/desktop_nav/desktop_nav.vue index 07bf8005..92622cf9 100644 --- a/src/components/desktop_nav/desktop_nav.vue +++ b/src/components/desktop_nav/desktop_nav.vue @@ -76,6 +76,18 @@ </button> </div> </div> + <teleport to="#modal"> + <confirm-modal + v-if="showingConfirmLogout" + :title="$t('login.logout_confirm_title')" + :confirm-text="$t('login.logout_confirm_accept_button')" + :cancel-text="$t('login.logout_confirm_cancel_button')" + @accepted="doLogout" + @cancelled="hideConfirmLogout" + > + {{ $t('login.logout_confirm') }} + </confirm-modal> + </teleport> </nav> </template> <script src="./desktop_nav.js"></script> diff --git a/src/components/dialog_modal/dialog_modal.vue b/src/components/dialog_modal/dialog_modal.vue index 24d65142..341cf105 100644 --- a/src/components/dialog_modal/dialog_modal.vue +++ b/src/components/dialog_modal/dialog_modal.vue @@ -39,7 +39,7 @@ right: 0; top: 0; background: rgb(27 31 35 / 50%); - z-index: 99; + z-index: 2000; } } @@ -51,7 +51,7 @@ margin: 15vh auto; position: fixed; transform: translateX(-50%); - z-index: 999; + z-index: 2001; cursor: default; display: block; background-color: $fallback--bg; diff --git a/src/components/extra_buttons/extra_buttons.js b/src/components/extra_buttons/extra_buttons.js index 3dc968c9..48b960b2 100644 --- a/src/components/extra_buttons/extra_buttons.js +++ b/src/components/extra_buttons/extra_buttons.js @@ -1,4 +1,5 @@ import Popover from '../popover/popover.vue' +import ConfirmModal from '../confirm_modal/confirm_modal.vue' import { library } from '@fortawesome/fontawesome-svg-core' import { faEllipsisH, @@ -32,10 +33,14 @@ library.add( const ExtraButtons = { props: ['status'], - components: { Popover }, + components: { + Popover, + ConfirmModal + }, data () { return { - expanded: false + expanded: false, + showingDeleteDialog: false } }, methods: { @@ -46,11 +51,22 @@ const ExtraButtons = { this.expanded = false }, deleteStatus () { - const confirmed = window.confirm(this.$t('status.delete_confirm')) - if (confirmed) { - this.$store.dispatch('deleteStatus', { id: this.status.id }) + if (this.shouldConfirmDelete) { + this.showDeleteStatusConfirmDialog() + } else { + this.doDeleteStatus() } }, + doDeleteStatus () { + this.$store.dispatch('deleteStatus', { id: this.status.id }) + this.hideDeleteStatusConfirmDialog() + }, + showDeleteStatusConfirmDialog () { + this.showingDeleteDialog = true + }, + hideDeleteStatusConfirmDialog () { + this.showingDeleteDialog = false + }, pinStatus () { this.$store.dispatch('pinStatus', this.status.id) .then(() => this.$emit('onSuccess')) @@ -133,7 +149,10 @@ const ExtraButtons = { isEdited () { return this.status.edited_at !== null }, - editingAvailable () { return this.$store.state.instance.editingAvailable } + editingAvailable () { return this.$store.state.instance.editingAvailable }, + shouldConfirmDelete () { + return this.$store.getters.mergedConfig.modalOnDelete + } } } diff --git a/src/components/extra_buttons/extra_buttons.vue b/src/components/extra_buttons/extra_buttons.vue index a84d47f6..c1c15c0f 100644 --- a/src/components/extra_buttons/extra_buttons.vue +++ b/src/components/extra_buttons/extra_buttons.vue @@ -165,6 +165,18 @@ /> </FALayers> </span> + <teleport to="#modal"> + <ConfirmModal + v-if="showingDeleteDialog" + :title="$t('status.delete_confirm_title')" + :cancel-text="$t('status.delete_confirm_cancel_button')" + :confirm-text="$t('status.delete_confirm_accept_button')" + @cancelled="hideDeleteStatusConfirmDialog" + @accepted="doDeleteStatus" + > + {{ $t('status.delete_confirm') }} + </ConfirmModal> + </teleport> </template> </Popover> </template> diff --git a/src/components/follow_button/follow_button.js b/src/components/follow_button/follow_button.js index 3edbcb86..443aa9bc 100644 --- a/src/components/follow_button/follow_button.js +++ b/src/components/follow_button/follow_button.js @@ -1,12 +1,20 @@ +import ConfirmModal from '../confirm_modal/confirm_modal.vue' import { requestFollow, requestUnfollow } from '../../services/follow_manipulate/follow_manipulate' export default { props: ['relationship', 'user', 'labelFollowing', 'buttonClass'], + components: { + ConfirmModal + }, data () { return { - inProgress: false + inProgress: false, + showingConfirmUnfollow: false } }, computed: { + shouldConfirmUnfollow () { + return this.$store.getters.mergedConfig.modalOnUnfollow + }, isPressed () { return this.inProgress || this.relationship.following }, @@ -35,6 +43,12 @@ export default { } }, methods: { + showConfirmUnfollow () { + this.showingConfirmUnfollow = true + }, + hideConfirmUnfollow () { + this.showingConfirmUnfollow = false + }, onClick () { this.relationship.following || this.relationship.requested ? this.unfollow() : this.follow() }, @@ -45,12 +59,21 @@ export default { }) }, unfollow () { + if (this.shouldConfirmUnfollow) { + this.showConfirmUnfollow() + } else { + this.doUnfollow() + } + }, + doUnfollow () { const store = this.$store this.inProgress = true requestUnfollow(this.relationship.id, store).then(() => { this.inProgress = false store.commit('removeStatus', { timeline: 'friends', userId: this.relationship.id }) }) + + this.hideConfirmUnfollow() } } } diff --git a/src/components/follow_button/follow_button.vue b/src/components/follow_button/follow_button.vue index 965d5256..e421c15b 100644 --- a/src/components/follow_button/follow_button.vue +++ b/src/components/follow_button/follow_button.vue @@ -7,6 +7,27 @@ @click="onClick" > {{ label }} + <teleport to="#modal"> + <confirm-modal + v-if="showingConfirmUnfollow" + :title="$t('user_card.unfollow_confirm_title')" + :confirm-text="$t('user_card.unfollow_confirm_accept_button')" + :cancel-text="$t('user_card.unfollow_confirm_cancel_button')" + @accepted="doUnfollow" + @cancelled="hideConfirmUnfollow" + > + <i18n-t + keypath="user_card.unfollow_confirm" + tag="span" + > + <template #user> + <span + v-text="user.screen_name_ui" + /> + </template> + </i18n-t> + </confirm-modal> + </teleport> </button> </template> diff --git a/src/components/follow_card/follow_card.vue b/src/components/follow_card/follow_card.vue index eff69fb2..bdb6b809 100644 --- a/src/components/follow_card/follow_card.vue +++ b/src/components/follow_card/follow_card.vue @@ -24,6 +24,7 @@ /> <RemoveFollowerButton v-if="noFollowsYou && relationship.followed_by" + :user="user" :relationship="relationship" class="follow-card-button" /> diff --git a/src/components/follow_request_card/follow_request_card.js b/src/components/follow_request_card/follow_request_card.js index cbd75311..b0873bb1 100644 --- a/src/components/follow_request_card/follow_request_card.js +++ b/src/components/follow_request_card/follow_request_card.js @@ -1,10 +1,18 @@ import BasicUserCard from '../basic_user_card/basic_user_card.vue' +import ConfirmModal from '../confirm_modal/confirm_modal.vue' import { notificationsFromStore } from '../../services/notification_utils/notification_utils.js' const FollowRequestCard = { props: ['user'], components: { - BasicUserCard + BasicUserCard, + ConfirmModal + }, + data () { + return { + showingApproveConfirmDialog: false, + showingDenyConfirmDialog: false + } }, methods: { findFollowRequestNotificationId () { @@ -13,7 +21,26 @@ const FollowRequestCard = { ) return notif && notif.id }, + showApproveConfirmDialog () { + this.showingApproveConfirmDialog = true + }, + hideApproveConfirmDialog () { + this.showingApproveConfirmDialog = false + }, + showDenyConfirmDialog () { + this.showingDenyConfirmDialog = true + }, + hideDenyConfirmDialog () { + this.showingDenyConfirmDialog = false + }, approveUser () { + if (this.shouldConfirmApprove) { + this.showApproveConfirmDialog() + } else { + this.doApprove() + } + }, + doApprove () { this.$store.state.api.backendInteractor.approveUser({ id: this.user.id }) this.$store.dispatch('removeFollowRequest', this.user) @@ -25,14 +52,34 @@ const FollowRequestCard = { notification.type = 'follow' } }) + this.hideApproveConfirmDialog() }, denyUser () { + if (this.shouldConfirmDeny) { + this.showDenyConfirmDialog() + } else { + this.doDeny() + } + }, + doDeny () { const notifId = this.findFollowRequestNotificationId() this.$store.state.api.backendInteractor.denyUser({ id: this.user.id }) .then(() => { this.$store.dispatch('dismissNotificationLocal', { id: notifId }) this.$store.dispatch('removeFollowRequest', this.user) }) + this.hideDenyConfirmDialog() + } + }, + computed: { + mergedConfig () { + return this.$store.getters.mergedConfig + }, + shouldConfirmApprove () { + return this.mergedConfig.modalOnApproveFollow + }, + shouldConfirmDeny () { + return this.mergedConfig.modalOnDenyFollow } } } diff --git a/src/components/follow_request_card/follow_request_card.vue b/src/components/follow_request_card/follow_request_card.vue index eb222cc7..55b65112 100644 --- a/src/components/follow_request_card/follow_request_card.vue +++ b/src/components/follow_request_card/follow_request_card.vue @@ -14,6 +14,28 @@ {{ $t('user_card.deny') }} </button> </div> + <teleport to="#modal"> + <confirm-modal + v-if="showingApproveConfirmDialog" + :title="$t('user_card.approve_confirm_title')" + :confirm-text="$t('user_card.approve_confirm_accept_button')" + :cancel-text="$t('user_card.approve_confirm_cancel_button')" + @accepted="doApprove" + @cancelled="hideApproveConfirmDialog" + > + {{ $t('user_card.approve_confirm', { user: user.screen_name_ui }) }} + </confirm-modal> + <confirm-modal + v-if="showingDenyConfirmDialog" + :title="$t('user_card.deny_confirm_title')" + :confirm-text="$t('user_card.deny_confirm_accept_button')" + :cancel-text="$t('user_card.deny_confirm_cancel_button')" + @accepted="doDeny" + @cancelled="hideDenyConfirmDialog" + > + {{ $t('user_card.deny_confirm', { user: user.screen_name_ui }) }} + </confirm-modal> + </teleport> </basic-user-card> </template> diff --git a/src/components/interface_language_switcher/interface_language_switcher.vue b/src/components/interface_language_switcher/interface_language_switcher.vue index 6997f149..c9e7506b 100644 --- a/src/components/interface_language_switcher/interface_language_switcher.vue +++ b/src/components/interface_language_switcher/interface_language_switcher.vue @@ -1,21 +1,44 @@ <template> - <div> - <label for="interface-language-switcher"> + <div class="interface-language-switcher"> + <label> {{ promptText }} </label> - {{ ' ' }} - <Select - id="interface-language-switcher" - v-model="controlledLanguage" - > - <option - v-for="lang in languages" - :key="lang.code" - :value="lang.code" + <ul class="setting-list"> + <li + v-for="index of controlledLanguage.keys()" + :key="index" > - {{ lang.name }} - </option> - </Select> + <label> + {{ index === 0 ? $t('settings.primary_language') : $tc('settings.fallback_language', index, { index }) }} + <Select + class="language-select" + :model-value="controlledLanguage[index]" + @update:modelValue="val => setLanguageAt(index, val)" + > + <option + v-for="lang in languages" + :key="lang.code" + :value="lang.code" + > + {{ lang.name }} + </option> + </Select> + </label> + <button + v-if="controlledLanguage.length > 1 && index !== 0" + class="button-default btn" + @click="() => removeLanguageAt(index)" + > + {{ $t('settings.remove_language') }} + </button> + </li> + <li> + <button + class="button-default btn" + @click="addLanguage" + >{{ $t('settings.add_language') }}</button> + </li> + </ul> </div> </template> @@ -34,7 +57,7 @@ export default { required: true }, language: { - type: String, + type: [Array, String], required: true }, setLanguage: { @@ -48,7 +71,9 @@ export default { }, controlledLanguage: { - get: function () { return this.language }, + get: function () { + return Array.isArray(this.language) ? this.language : [this.language] + }, set: function (val) { this.setLanguage(val) } @@ -58,7 +83,30 @@ export default { methods: { getLanguageName (code) { return localeService.getLanguageName(code) + }, + addLanguage () { + this.controlledLanguage = [...this.controlledLanguage, ''] + }, + setLanguageAt (index, val) { + const lang = [...this.controlledLanguage] + lang[index] = val + this.controlledLanguage = lang + }, + removeLanguageAt (index) { + const lang = [...this.controlledLanguage] + lang.splice(index, 1) + this.controlledLanguage = lang } } } </script> + +<style lang="scss"> +@import '../../_variables.scss'; + +.interface-language-switcher { + .language-select { + margin-right: 1em; + } +} +</style> diff --git a/src/components/mobile_nav/mobile_nav.js b/src/components/mobile_nav/mobile_nav.js index cdbbb812..dad1f6aa 100644 --- a/src/components/mobile_nav/mobile_nav.js +++ b/src/components/mobile_nav/mobile_nav.js @@ -1,5 +1,6 @@ import SideDrawer from '../side_drawer/side_drawer.vue' import Notifications from '../notifications/notifications.vue' +import ConfirmModal from '../confirm_modal/confirm_modal.vue' import { unseenNotificationsFromStore } from '../../services/notification_utils/notification_utils' import GestureService from '../../services/gesture_service/gesture_service' import NavigationPins from 'src/components/navigation/navigation_pins.vue' @@ -25,12 +26,14 @@ const MobileNav = { components: { SideDrawer, Notifications, - NavigationPins + NavigationPins, + ConfirmModal }, data: () => ({ notificationsCloseGesture: undefined, notificationsOpen: false, - notificationsAtTop: true + notificationsAtTop: true, + showingConfirmLogout: false }), created () { this.notificationsCloseGesture = GestureService.swipeGesture( @@ -57,7 +60,11 @@ const MobileNav = { ...mapGetters(['unreadChatCount', 'unreadAnnouncementCount']), chatsPinned () { return new Set(this.$store.state.serverSideStorage.prefsStorage.collections.pinnedNavItems).has('chats') - } + }, + shouldConfirmLogout () { + return this.$store.getters.mergedConfig.modalOnLogout + }, + ...mapGetters(['unreadChatCount']) }, methods: { toggleMobileSidebar () { @@ -88,9 +95,23 @@ const MobileNav = { scrollMobileNotificationsToTop () { this.$refs.mobileNotifications.scrollTo(0, 0) }, + showConfirmLogout () { + this.showingConfirmLogout = true + }, + hideConfirmLogout () { + this.showingConfirmLogout = false + }, logout () { + if (!this.shouldConfirmLogout) { + this.doLogout() + } else { + this.showConfirmLogout() + } + }, + doLogout () { this.$router.replace('/main/public') this.$store.dispatch('logout') + this.hideConfirmLogout() }, markNotificationsAsSeen () { // this.$refs.notifications.markAsSeen() diff --git a/src/components/mobile_nav/mobile_nav.vue b/src/components/mobile_nav/mobile_nav.vue index d6fe102c..c2746abe 100644 --- a/src/components/mobile_nav/mobile_nav.vue +++ b/src/components/mobile_nav/mobile_nav.vue @@ -88,6 +88,18 @@ ref="sideDrawer" :logout="logout" /> + <teleport to="#modal"> + <confirm-modal + v-if="showingConfirmLogout" + :title="$t('login.logout_confirm_title')" + :confirm-text="$t('login.logout_confirm_accept_button')" + :cancel-text="$t('login.logout_confirm_cancel_button')" + @accepted="doLogout" + @cancelled="hideConfirmLogout" + > + {{ $t('login.logout_confirm') }} + </confirm-modal> + </teleport> </div> </template> @@ -235,6 +247,16 @@ } } } + + .confirm-modal.dark-overlay { + &::before { + z-index: 3000; + } + + .dialog-modal.panel { + z-index: 3001; + } + } } </style> diff --git a/src/components/notification/notification.js b/src/components/notification/notification.js index 265aaee0..420db4f0 100644 --- a/src/components/notification/notification.js +++ b/src/components/notification/notification.js @@ -8,6 +8,7 @@ import Report from '../report/report.vue' import UserLink from '../user_link/user_link.vue' import RichContent from 'src/components/rich_content/rich_content.jsx' import UserPopover from '../user_popover/user_popover.vue' +import ConfirmModal from '../confirm_modal/confirm_modal.vue' import { isStatusNotification } from '../../services/notification_utils/notification_utils.js' import { highlightClass, highlightStyle } from '../../services/user_highlighter/user_highlighter.js' import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator' @@ -43,7 +44,9 @@ const Notification = { return { statusExpanded: false, betterShadow: this.$store.state.interface.browserSupport.cssFilter, - unmuted: false + unmuted: false, + showingApproveConfirmDialog: false, + showingDenyConfirmDialog: false } }, props: ['notification'], @@ -56,7 +59,8 @@ const Notification = { Report, RichContent, UserPopover, - UserLink + UserLink, + ConfirmModal }, methods: { toggleStatusExpanded () { @@ -71,7 +75,26 @@ const Notification = { toggleMute () { this.unmuted = !this.unmuted }, + showApproveConfirmDialog () { + this.showingApproveConfirmDialog = true + }, + hideApproveConfirmDialog () { + this.showingApproveConfirmDialog = false + }, + showDenyConfirmDialog () { + this.showingDenyConfirmDialog = true + }, + hideDenyConfirmDialog () { + this.showingDenyConfirmDialog = false + }, approveUser () { + if (this.shouldConfirmApprove) { + this.showApproveConfirmDialog() + } else { + this.doApprove() + } + }, + doApprove () { this.$store.state.api.backendInteractor.approveUser({ id: this.user.id }) this.$store.dispatch('removeFollowRequest', this.user) this.$store.dispatch('markSingleNotificationAsSeen', { id: this.notification.id }) @@ -81,13 +104,22 @@ const Notification = { notification.type = 'follow' } }) + this.hideApproveConfirmDialog() }, denyUser () { + if (this.shouldConfirmDeny) { + this.showDenyConfirmDialog() + } else { + this.doDeny() + } + }, + doDeny () { this.$store.state.api.backendInteractor.denyUser({ id: this.user.id }) .then(() => { this.$store.dispatch('dismissNotificationLocal', { id: this.notification.id }) this.$store.dispatch('removeFollowRequest', this.user) }) + this.hideDenyConfirmDialog() } }, computed: { @@ -117,6 +149,15 @@ const Notification = { isStatusNotification () { return isStatusNotification(this.notification.type) }, + mergedConfig () { + return this.$store.getters.mergedConfig + }, + shouldConfirmApprove () { + return this.mergedConfig.modalOnApproveFollow + }, + shouldConfirmDeny () { + return this.mergedConfig.modalOnDenyFollow + }, ...mapState({ currentUser: state => state.users.currentUser }) diff --git a/src/components/notification/notification.vue b/src/components/notification/notification.vue index f1aa5420..e1ea42ad 100644 --- a/src/components/notification/notification.vue +++ b/src/components/notification/notification.vue @@ -243,6 +243,28 @@ </template> </div> </div> + <teleport to="#modal"> + <confirm-modal + v-if="showingApproveConfirmDialog" + :title="$t('user_card.approve_confirm_title')" + :confirm-text="$t('user_card.approve_confirm_accept_button')" + :cancel-text="$t('user_card.approve_confirm_cancel_button')" + @accepted="doApprove" + @cancelled="hideApproveConfirmDialog" + > + {{ $t('user_card.approve_confirm', { user: user.screen_name_ui }) }} + </confirm-modal> + <confirm-modal + v-if="showingDenyConfirmDialog" + :title="$t('user_card.deny_confirm_title')" + :confirm-text="$t('user_card.deny_confirm_accept_button')" + :cancel-text="$t('user_card.deny_confirm_cancel_button')" + @accepted="doDeny" + @cancelled="hideDenyConfirmDialog" + > + {{ $t('user_card.deny_confirm', { user: user.screen_name_ui }) }} + </confirm-modal> + </teleport> </article> </template> diff --git a/src/components/poll/poll_form.js b/src/components/poll/poll_form.js index e30645c3..a2070155 100644 --- a/src/components/poll/poll_form.js +++ b/src/components/poll/poll_form.js @@ -94,19 +94,10 @@ export default { }, convertExpiryToUnit (unit, amount) { // Note: we want seconds and not milliseconds - switch (unit) { - case 'minutes': return (1000 * amount) / DateUtils.MINUTE - case 'hours': return (1000 * amount) / DateUtils.HOUR - case 'days': return (1000 * amount) / DateUtils.DAY - } + return DateUtils.secondsToUnit(unit, amount) }, 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 - } + return DateUtils.unitToSeconds(unit, amount) }, expiryAmountChange () { this.expiryAmount = diff --git a/src/components/remove_follower_button/remove_follower_button.js b/src/components/remove_follower_button/remove_follower_button.js index e1a7531b..052a519f 100644 --- a/src/components/remove_follower_button/remove_follower_button.js +++ b/src/components/remove_follower_button/remove_follower_button.js @@ -1,10 +1,16 @@ +import ConfirmModal from '../confirm_modal/confirm_modal.vue' + export default { - props: ['relationship'], + props: ['user', 'relationship'], data () { return { - inProgress: false + inProgress: false, + showingConfirmRemoveFollower: false } }, + components: { + ConfirmModal + }, computed: { label () { if (this.inProgress) { @@ -12,14 +18,31 @@ export default { } else { return this.$t('user_card.remove_follower') } + }, + shouldConfirmRemoveUserFromFollowers () { + return this.$store.getters.mergedConfig.modalOnRemoveUserFromFollowers } }, methods: { + showConfirmRemoveUserFromFollowers () { + this.showingConfirmRemoveFollower = true + }, + hideConfirmRemoveUserFromFollowers () { + this.showingConfirmRemoveFollower = false + }, onClick () { + if (!this.shouldConfirmRemoveUserFromFollowers) { + this.doRemoveUserFromFollowers() + } else { + this.showConfirmRemoveUserFromFollowers() + } + }, + doRemoveUserFromFollowers () { this.inProgress = true this.$store.dispatch('removeUserFromFollowers', this.relationship.id).then(() => { this.inProgress = false }) + this.hideConfirmRemoveUserFromFollowers() } } } diff --git a/src/components/remove_follower_button/remove_follower_button.vue b/src/components/remove_follower_button/remove_follower_button.vue index a3a4c242..0012aebd 100644 --- a/src/components/remove_follower_button/remove_follower_button.vue +++ b/src/components/remove_follower_button/remove_follower_button.vue @@ -7,6 +7,27 @@ @click="onClick" > {{ label }} + <teleport to="#modal"> + <confirm-modal + v-if="showingConfirmRemoveFollower" + :title="$t('user_card.remove_follower_confirm_title')" + :confirm-text="$t('user_card.remove_follower_confirm_accept_button')" + :cancel-text="$t('user_card.remove_follower_confirm_cancel_button')" + @accepted="doRemoveUserFromFollowers" + @cancelled="hideConfirmRemoveUserFromFollowers" + > + <i18n-t + keypath="user_card.remove_follower_confirm" + tag="span" + > + <template #user> + <span + v-text="user.screen_name_ui" + /> + </template> + </i18n-t> + </confirm-modal> + </teleport> </button> </template> diff --git a/src/components/retweet_button/retweet_button.js b/src/components/retweet_button/retweet_button.js index 4d92b5fa..198b6c14 100644 --- a/src/components/retweet_button/retweet_button.js +++ b/src/components/retweet_button/retweet_button.js @@ -1,3 +1,4 @@ +import ConfirmModal from '../confirm_modal/confirm_modal.vue' import { library } from '@fortawesome/fontawesome-svg-core' import { faRetweet, @@ -15,13 +16,24 @@ library.add( const RetweetButton = { props: ['status', 'loggedIn', 'visibility'], + components: { + ConfirmModal + }, data () { return { - animated: false + animated: false, + showingConfirmDialog: false } }, methods: { retweet () { + if (!this.status.repeated && this.shouldConfirmRepeat) { + this.showConfirmDialog() + } else { + this.doRetweet() + } + }, + doRetweet () { if (!this.status.repeated) { this.$store.dispatch('retweet', { id: this.status.id }) } else { @@ -31,6 +43,13 @@ const RetweetButton = { setTimeout(() => { this.animated = false }, 500) + this.hideConfirmDialog() + }, + showConfirmDialog () { + this.showingConfirmDialog = true + }, + hideConfirmDialog () { + this.showingConfirmDialog = false } }, computed: { @@ -39,6 +58,9 @@ const RetweetButton = { }, remoteInteractionLink () { return this.$store.getters.remoteInteractionLink({ statusId: this.status.id }) + }, + shouldConfirmRepeat () { + return this.mergedConfig.modalOnRepeat } } } diff --git a/src/components/retweet_button/retweet_button.vue b/src/components/retweet_button/retweet_button.vue index 7700ee0d..134fcb36 100644 --- a/src/components/retweet_button/retweet_button.vue +++ b/src/components/retweet_button/retweet_button.vue @@ -59,6 +59,18 @@ > {{ status.repeat_num }} </span> + <teleport to="#modal"> + <confirm-modal + v-if="showingConfirmDialog" + :title="$t('status.repeat_confirm_title')" + :confirm-text="$t('status.repeat_confirm_accept_button')" + :cancel-text="$t('status.repeat_confirm_cancel_button')" + @accepted="doRetweet" + @cancelled="hideConfirmDialog" + > + {{ $t('status.repeat_confirm') }} + </confirm-modal> + </teleport> </div> </template> diff --git a/src/components/settings_modal/tabs/general_tab.vue b/src/components/settings_modal/tabs/general_tab.vue index 582cb288..703e94a0 100644 --- a/src/components/settings_modal/tabs/general_tab.vue +++ b/src/components/settings_modal/tabs/general_tab.vue @@ -148,6 +148,56 @@ </SizeSetting> </div> </li> + <li class="select-multiple"> + <span class="label">{{ $t('settings.confirm_dialogs') }}</span> + <ul class="option-list"> + <li> + <BooleanSetting path="modalOnRepeat"> + {{ $t('settings.confirm_dialogs_repeat') }} + </BooleanSetting> + </li> + <li> + <BooleanSetting path="modalOnUnfollow"> + {{ $t('settings.confirm_dialogs_unfollow') }} + </BooleanSetting> + </li> + <li> + <BooleanSetting path="modalOnBlock"> + {{ $t('settings.confirm_dialogs_block') }} + </BooleanSetting> + </li> + <li> + <BooleanSetting path="modalOnMute"> + {{ $t('settings.confirm_dialogs_mute') }} + </BooleanSetting> + </li> + <li> + <BooleanSetting path="modalOnDelete"> + {{ $t('settings.confirm_dialogs_delete') }} + </BooleanSetting> + </li> + <li> + <BooleanSetting path="modalOnLogout"> + {{ $t('settings.confirm_dialogs_logout') }} + </BooleanSetting> + </li> + <li> + <BooleanSetting path="modalOnApproveFollow"> + {{ $t('settings.confirm_dialogs_approve_follow') }} + </BooleanSetting> + </li> + <li> + <BooleanSetting path="modalOnDenyFollow"> + {{ $t('settings.confirm_dialogs_deny_follow') }} + </BooleanSetting> + </li> + <li> + <BooleanSetting path="modalOnRemoveUserFromFollowers"> + {{ $t('settings.confirm_dialogs_remove_follower') }} + </BooleanSetting> + </li> + </ul> + </li> </ul> </div> <div class="setting-item"> diff --git a/src/components/settings_modal/tabs/profile_tab.js b/src/components/settings_modal/tabs/profile_tab.js index df100162..72d27083 100644 --- a/src/components/settings_modal/tabs/profile_tab.js +++ b/src/components/settings_modal/tabs/profile_tab.js @@ -45,7 +45,7 @@ const ProfileTab = { bannerPreview: null, background: null, backgroundPreview: null, - emailLanguage: this.$store.state.users.currentUser.language || '' + emailLanguage: this.$store.state.users.currentUser.language || [''] } }, components: { @@ -134,7 +134,7 @@ const ProfileTab = { } if (this.emailLanguage) { - params.language = localeService.internalToBackendLocale(this.emailLanguage) + params.language = localeService.internalToBackendLocaleMulti(this.emailLanguage) } this.$store.state.api.backendInteractor diff --git a/src/components/user_card/user_card.js b/src/components/user_card/user_card.js index 67879307..e17bf8eb 100644 --- a/src/components/user_card/user_card.js +++ b/src/components/user_card/user_card.js @@ -1,3 +1,4 @@ +import { unitToSeconds } from 'src/services/date_utils/date_utils.js' import UserAvatar from '../user_avatar/user_avatar.vue' import RemoteFollow from '../remote_follow/remote_follow.vue' import ProgressButton from '../progress_button/progress_button.vue' @@ -8,6 +9,7 @@ import UserNote from '../user_note/user_note.vue' import Select from '../select/select.vue' import UserLink from '../user_link/user_link.vue' import RichContent from 'src/components/rich_content/rich_content.jsx' +import ConfirmModal from '../confirm_modal/confirm_modal.vue' import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator' import { mapGetters } from 'vuex' import { library } from '@fortawesome/fontawesome-svg-core' @@ -46,7 +48,10 @@ export default { data () { return { followRequestInProgress: false, - betterShadow: this.$store.state.interface.browserSupport.cssFilter + betterShadow: this.$store.state.interface.browserSupport.cssFilter, + showingConfirmMute: false, + muteExpiryAmount: 0, + muteExpiryUnit: 'minutes' } }, created () { @@ -137,6 +142,12 @@ export default { supportsNote () { return 'note' in this.relationship }, + shouldConfirmMute () { + return this.mergedConfig.modalOnMute + }, + muteExpiryUnits () { + return ['minutes', 'hours', 'days'] + }, ...mapGetters(['mergedConfig']) }, components: { @@ -149,11 +160,29 @@ export default { Select, RichContent, UserLink, - UserNote + UserNote, + ConfirmModal }, methods: { + showConfirmMute () { + this.showingConfirmMute = true + }, + hideConfirmMute () { + this.showingConfirmMute = false + }, muteUser () { - this.$store.dispatch('muteUser', this.user.id) + if (!this.shouldConfirmMute) { + this.doMuteUser() + } else { + this.showConfirmMute() + } + }, + doMuteUser () { + this.$store.dispatch('muteUser', { + id: this.user.id, + expiresIn: this.shouldConfirmMute ? unitToSeconds(this.muteExpiryUnit, this.muteExpiryAmount) : 0 + }) + this.hideConfirmMute() }, unmuteUser () { this.$store.dispatch('unmuteUser', this.user.id) diff --git a/src/components/user_card/user_card.scss b/src/components/user_card/user_card.scss index d56b6672..4ab93a8a 100644 --- a/src/components/user_card/user_card.scss +++ b/src/components/user_card/user_card.scss @@ -355,3 +355,8 @@ text-decoration: none; } } + +.mute-expiry { + display: flex; + flex-direction: row; +} diff --git a/src/components/user_card/user_card.vue b/src/components/user_card/user_card.vue index 349c7cb1..2de14063 100644 --- a/src/components/user_card/user_card.vue +++ b/src/components/user_card/user_card.vue @@ -314,6 +314,53 @@ :handle-links="true" /> </div> + <teleport to="#modal"> + <confirm-modal + v-if="showingConfirmMute" + :title="$t('user_card.mute_confirm_title')" + :confirm-text="$t('user_card.mute_confirm_accept_button')" + :cancel-text="$t('user_card.mute_confirm_cancel_button')" + @accepted="doMuteUser" + @cancelled="hideConfirmMute" + > + <i18n-t + keypath="user_card.mute_confirm" + tag="div" + > + <template #user> + <span + v-text="user.screen_name_ui" + /> + </template> + </i18n-t> + <div + class="mute-expiry" + > + <label> + {{ $t('user_card.mute_duration_prompt') }} + </label> + <input + v-model="muteExpiryAmount" + type="number" + class="expiry-amount hide-number-spinner" + :min="0" + > + <Select + v-model="muteExpiryUnit" + unstyled="true" + class="expiry-unit" + > + <option + v-for="unit in muteExpiryUnits" + :key="unit" + :value="unit" + > + {{ $t(`time.${unit}_short`, ['']) }} + </option> + </Select> + </div> + </confirm-modal> + </teleport> </div> </template> diff --git a/src/i18n/en.json b/src/i18n/en.json index 15d3bec4..0bdf4051 100644 --- a/src/i18n/en.json +++ b/src/i18n/en.json @@ -137,6 +137,10 @@ "login": "Log in", "description": "Log in with OAuth", "logout": "Log out", + "logout_confirm_title": "Logout confirmation", + "logout_confirm": "Do you really want to logout?", + "logout_confirm_accept_button": "Logout", + "logout_confirm_cancel_button": "Do not logout", "password": "Password", "placeholder": "e.g. lain", "register": "Register", @@ -339,6 +343,10 @@ "select_all": "Select all" }, "settings": { + "add_language": "Add fallback language", + "remove_language": "Remove", + "primary_language": "Primary language:", + "fallback_language": "Fallback language {index}:", "app_name": "App name", "expert_mode": "Show advanced", "save": "Save changes", @@ -420,6 +428,16 @@ "composing": "Composing", "confirm_new_password": "Confirm new password", "current_password": "Current password", + "confirm_dialogs": "Ask for confirmation when", + "confirm_dialogs_repeat": "repeating a status", + "confirm_dialogs_unfollow": "unfollowing a user", + "confirm_dialogs_block": "blocking a user", + "confirm_dialogs_mute": "muting a user", + "confirm_dialogs_delete": "deleting a status", + "confirm_dialogs_logout": "logging out", + "confirm_dialogs_approve_follow": "approving a follower", + "confirm_dialogs_deny_follow": "denying a follower", + "confirm_dialogs_remove_follower": "removing a follower", "mutes_and_blocks": "Mutes and Blocks", "data_import_export_tab": "Data import / export", "default_vis": "Default visibility scope", @@ -851,6 +869,10 @@ "status": { "favorites": "Favorites", "repeats": "Repeats", + "repeat_confirm": "Do you really want to repeat this status?", + "repeat_confirm_title": "Repeat confirmation", + "repeat_confirm_accept_button": "Repeat", + "repeat_confirm_cancel_button": "Do not repeat", "delete": "Delete status", "edit": "Edit status", "edited_at": "(last edited {time})", @@ -860,6 +882,9 @@ "bookmark": "Bookmark", "unbookmark": "Unbookmark", "delete_confirm": "Do you really want to delete this status?", + "delete_confirm_title": "Delete confirmation", + "delete_confirm_accept_button": "Delete", + "delete_confirm_cancel_button": "Keep", "reply_to": "Reply to", "mentions": "Mentions", "replies_list": "Replies:", @@ -906,10 +931,22 @@ }, "user_card": { "approve": "Approve", + "approve_confirm_title": "Approve confirmation", + "approve_confirm_accept_button": "Approve", + "approve_confirm_cancel_button": "Do not approve", + "approve_confirm": "Do you want to approve {user}'s follow request?", "block": "Block", "blocked": "Blocked!", + "block_confirm_title": "Block confirmation", + "block_confirm": "Do you really want to block {user}?", + "block_confirm_accept_button": "Block", + "block_confirm_cancel_button": "Do not block", "deactivated": "Deactivated", "deny": "Deny", + "deny_confirm_title": "Deny confirmation", + "deny_confirm_accept_button": "Deny", + "deny_confirm_cancel_button": "Do not deny", + "deny_confirm": "Do you want to deny {user}'s follow request?", "edit_profile": "Edit profile", "favorites": "Favorites", "follow": "Follow", @@ -917,6 +954,10 @@ "follow_sent": "Request sent!", "follow_progress": "Requesting…", "follow_unfollow": "Unfollow", + "unfollow_confirm_title": "Unfollow confirmation", + "unfollow_confirm": "Do you really want to unfollow {user}?", + "unfollow_confirm_accept_button": "Unfollow", + "unfollow_confirm_cancel_button": "Do not unfollow", "followees": "Following", "followers": "Followers", "following": "Following!", @@ -928,9 +969,18 @@ "message": "Message", "mute": "Mute", "muted": "Muted", + "mute_confirm_title": "Mute confirmation", + "mute_confirm": "Do you really want to mute {user}?", + "mute_confirm_accept_button": "Mute", + "mute_confirm_cancel_button": "Do not mute", + "mute_duration_prompt": "Mute this user for (0 for indefinite time):", "per_day": "per day", "remote_follow": "Remote follow", "remove_follower": "Remove follower", + "remove_follower_confirm_title": "Remove follower confirmation", + "remove_follower_confirm_accept_button": "Remove", + "remove_follower_confirm_cancel_button": "Keep", + "remove_follower_confirm": "Do you really want to remove {user} from your followers?", "report": "Report", "statuses": "Statuses", "subscribe": "Subscribe", diff --git a/src/i18n/eo.json b/src/i18n/eo.json index 3596065c..e013edee 100644 --- a/src/i18n/eo.json +++ b/src/i18n/eo.json @@ -603,7 +603,7 @@ "use_websockets": "Uzi teĥnikaron «websockets» (tuja ĝisdatigo)", "mention_link_display_full_for_remote": "plene nur je uzantoj foraj (ekz. {'@'}zozo{'@'}ekzemplo.org)", "expert_mode": "Montri altnivelajn", - "setting_server_side": "Ĉi tiu agordo estas ligita al via profilo, kaj efektiviĝon en ĉiuj viaj salutoj kaj klientoj", + "setting_server_side": "Ĉi tiu agordo estas ligita al via profilo, kaj efektiviĝos en ĉiuj viaj salutoj kaj klientoj", "post_look_feel": "Aspekto de afiŝoj", "mention_links": "Menciaj ligiloj", "email_language": "Lingvo de leteroj ricevotaj de la servilo", diff --git a/src/i18n/messages.js b/src/i18n/messages.js index 74a89ca8..849d98fd 100644 --- a/src/i18n/messages.js +++ b/src/i18n/messages.js @@ -7,8 +7,11 @@ // sed -i -e "s/'//gm" -e 's/"/\\"/gm' -re 's/^( +)(.+?): ((.+?))?(,?)(\{?)$/\1"\2": "\4"/gm' -e 's/\"\{\"/{/g' -e 's/,"$/",/g' file.json // There's only problem that apostrophe character ' gets replaced by \\ so you have to fix it manually, sorry. +import { isEqual } from 'lodash' import { languages, langCodeToJsonName } from './languages.js' +const ULTIMATE_FALLBACK_LOCALE = 'en' + const hasLanguageFile = (code) => languages.includes(code) const loadLanguageFile = (code) => { @@ -25,11 +28,26 @@ const messages = { en: require('./en.json').default }, setLanguage: async (i18n, language) => { - if (hasLanguageFile(language)) { - const messages = await loadLanguageFile(language) - i18n.setLocaleMessage(language, messages.default) + const languages = (Array.isArray(language) ? language : [language]).filter(k => k) + + if (!languages.includes(ULTIMATE_FALLBACK_LOCALE)) { + languages.push(ULTIMATE_FALLBACK_LOCALE) + } + const [first, ...rest] = languages + + if (first === i18n.locale && isEqual(rest, i18n.fallbackLocale)) { + return } - i18n.locale = language + + for (const lang of languages) { + if (hasLanguageFile(lang)) { + const messages = await loadLanguageFile(lang) + i18n.setLocaleMessage(lang, messages.default) + } + } + + i18n.fallbackLocale = rest + i18n.locale = first } } diff --git a/src/i18n/zh.json b/src/i18n/zh.json index cf5f384c..8f0b04bd 100644 --- a/src/i18n/zh.json +++ b/src/i18n/zh.json @@ -53,7 +53,13 @@ "direct": "私讯", "private": "仅关注者", "unlisted": "列外" - } + }, + "scroll_to_top": "滚动至顶", + "generic_error_message": "发生一个错误:{0}", + "never_show_again": "不再显示", + "undo": "撤销", + "yes": "是", + "no": "否" }, "image_cropper": { "crop_picture": "裁剪图片", @@ -109,7 +115,12 @@ "chats": "聊天", "timelines": "时间线", "bookmarks": "书签", - "home_timeline": "主页时间线" + "home_timeline": "主页时间线", + "lists": "列表", + "edit_finish": "完成编辑", + "mobile_notifications": "打开通知(有未读的)", + "mobile_notifications_close": "关闭通知", + "announcements": "公告" }, "notifications": { "broken_favorite": "未知的状态,正在搜索中…", @@ -890,7 +901,18 @@ "search_emoji": "搜索表情符号", "emoji": "表情符号", "load_all": "加载所有表情符号(共 {emojiAmount} 个)", - "load_all_hint": "最先加载的 {saneAmount} 表情符号,加载全部表情符号可能会带来性能问题。" + "load_all_hint": "最先加载的 {saneAmount} 表情符号,加载全部表情符号可能会带来性能问题。", + "unicode_groups": { + "flags": "旗帜", + "food-and-drink": "饮食", + "objects": "物件", + "people-and-body": "人和身体", + "symbols": "符号", + "travel-and-places": "旅行和地点", + "activities": "活动", + "animals-and-nature": "动物和自然" + }, + "regional_indicator": "地区指示符 {letter}" }, "about": { "mrf": { @@ -958,5 +980,26 @@ "delete": "删除", "message_user": "发消息给 {nickname}", "you": "你:" + }, + "announcements": { + "page_header": "公告", + "title": "公告", + "mark_as_read_action": "标为已读", + "post_form_header": "发布公告", + "post_placeholder": "在这里输入公告内容...", + "post_action": "发布", + "post_error": "错误:{error}", + "close_error": "关闭", + "delete_action": "删除", + "start_time_prompt": "起始时间: ", + "end_time_prompt": "终止时间: ", + "all_day_prompt": "这是全天的事件", + "published_time_display": "发表于 {time}", + "start_time_display": "开始于 {time}", + "end_time_display": "结束于 {time}", + "edit_action": "编辑", + "submit_edit_action": "提交", + "cancel_edit_action": "取消", + "inactive_message": "这个公告不活跃" } } diff --git a/src/modules/config.js b/src/modules/config.js index 3cd6888f..eb33f95f 100644 --- a/src/modules/config.js +++ b/src/modules/config.js @@ -78,6 +78,15 @@ export const defaultState = { minimalScopesMode: undefined, // instance default // This hides statuses filtered via a word filter hideFilteredStatuses: undefined, // instance default + modalOnRepeat: undefined, // instance default + modalOnUnfollow: undefined, // instance default + modalOnBlock: undefined, // instance default + modalOnMute: undefined, // instance default + modalOnDelete: undefined, // instance default + modalOnLogout: undefined, // instance default + modalOnApproveFollow: undefined, // instance default + modalOnDenyFollow: undefined, // instance default + modalOnRemoveUserFromFollowers: undefined, // instance default playVideosInModal: false, useOneClickNsfw: false, useContainFit: true, @@ -184,7 +193,10 @@ const config = { case 'interfaceLanguage': messages.setLanguage(this.getters.i18n, value) dispatch('loadUnicodeEmojiData', value) - Cookies.set(BACKEND_LANGUAGE_COOKIE_NAME, localeService.internalToBackendLocale(value)) + Cookies.set( + BACKEND_LANGUAGE_COOKIE_NAME, + localeService.internalToBackendLocaleMulti(value) + ) break case 'thirdColumnMode': dispatch('setLayoutWidth', undefined) diff --git a/src/modules/instance.js b/src/modules/instance.js index 72694e2c..16f72583 100644 --- a/src/modules/instance.js +++ b/src/modules/instance.js @@ -71,6 +71,15 @@ const defaultState = { hideSitename: false, hideUserStats: false, muteBotStatuses: false, + modalOnRepeat: false, + modalOnUnfollow: false, + modalOnBlock: true, + modalOnMute: false, + modalOnDelete: true, + modalOnLogout: true, + modalOnApproveFollow: false, + modalOnDenyFollow: false, + modalOnRemoveUserFromFollowers: false, loginMethod: 'password', logo: '/static/logo.svg', logoMargin: '.2em', @@ -288,8 +297,13 @@ const instance = { langList .map(async lang => { if (!state.unicodeEmojiAnnotations[lang]) { - const annotations = await loadAnnotations(lang) - commit('setUnicodeEmojiAnnotations', { lang, annotations }) + try { + const annotations = await loadAnnotations(lang) + commit('setUnicodeEmojiAnnotations', { lang, annotations }) + } catch (e) { + console.warn(`Error loading unicode emoji annotations for ${lang}: `, e) + // ignore + } } })) }, diff --git a/src/modules/users.js b/src/modules/users.js index 053e44b6..a1316ba2 100644 --- a/src/modules/users.js +++ b/src/modules/users.js @@ -61,13 +61,16 @@ const editUserNote = (store, { id, comment }) => { .then((relationship) => store.commit('updateUserRelationship', [relationship])) } -const muteUser = (store, id) => { +const muteUser = (store, args) => { + const id = typeof args === 'object' ? args.id : args + const expiresIn = typeof args === 'object' ? args.expiresIn : 0 + const predictedRelationship = store.state.relationships[id] || { id } predictedRelationship.muting = true store.commit('updateUserRelationship', [predictedRelationship]) store.commit('addMuteId', id) - return store.rootState.api.backendInteractor.muteUser({ id }) + return store.rootState.api.backendInteractor.muteUser({ id, expiresIn }) .then((relationship) => { store.commit('updateUserRelationship', [relationship]) store.commit('addMuteId', id) diff --git a/src/services/api/api.service.js b/src/services/api/api.service.js index af12265e..b8c10b21 100644 --- a/src/services/api/api.service.js +++ b/src/services/api/api.service.js @@ -1118,8 +1118,12 @@ const fetchMutes = ({ credentials }) => { .then((users) => users.map(parseUser)) } -const muteUser = ({ id, credentials }) => { - return promisedRequest({ url: MASTODON_MUTE_USER_URL(id), credentials, method: 'POST' }) +const muteUser = ({ id, expiresIn, credentials }) => { + const payload = {} + if (expiresIn) { + payload.expires_in = expiresIn + } + return promisedRequest({ url: MASTODON_MUTE_USER_URL(id), credentials, method: 'POST', payload }) } const unmuteUser = ({ id, credentials }) => { diff --git a/src/services/date_utils/date_utils.js b/src/services/date_utils/date_utils.js index c93d2176..ed8e1417 100644 --- a/src/services/date_utils/date_utils.js +++ b/src/services/date_utils/date_utils.js @@ -41,3 +41,19 @@ export const relativeTimeShort = (date, nowThreshold = 1) => { r.key += '_short' return r } + +export const unitToSeconds = (unit, amount) => { + switch (unit) { + case 'minutes': return 0.001 * amount * MINUTE + case 'hours': return 0.001 * amount * HOUR + case 'days': return 0.001 * amount * DAY + } +} + +export const secondsToUnit = (unit, amount) => { + switch (unit) { + case 'minutes': return (1000 * amount) / MINUTE + case 'hours': return (1000 * amount) / HOUR + case 'days': return (1000 * amount) / DAY + } +} diff --git a/src/services/locale/locale.service.js b/src/services/locale/locale.service.js index d3389785..a4af8b90 100644 --- a/src/services/locale/locale.service.js +++ b/src/services/locale/locale.service.js @@ -11,6 +11,10 @@ const specialLanguageCodes = { const internalToBrowserLocale = code => specialLanguageCodes[code] || code const internalToBackendLocale = code => internalToBrowserLocale(code).replace('_', '-') +const internalToBackendLocaleMulti = codes => { + const langs = Array.isArray(codes) ? codes : [codes] + return langs.map(internalToBackendLocale).join(',') +} const getLanguageName = (code) => { const specialLanguageNames = { @@ -28,6 +32,7 @@ const languages = _.map(languagesObject.languages, (code) => ({ code, name: getL const localeService = { internalToBrowserLocale, internalToBackendLocale, + internalToBackendLocaleMulti, languages, getLanguageName } |
