diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/boot/after_store.js | 3 | ||||
| -rw-r--r-- | src/components/announcement/announcement.js | 3 | ||||
| -rw-r--r-- | src/components/announcement/announcement.vue | 4 | ||||
| -rw-r--r-- | src/components/announcements_page/announcements_page.js | 3 | ||||
| -rw-r--r-- | src/components/announcements_page/announcements_page.vue | 2 | ||||
| -rw-r--r-- | src/components/emoji_picker/emoji_picker.js | 123 | ||||
| -rw-r--r-- | src/components/emoji_picker/emoji_picker.scss | 1 | ||||
| -rw-r--r-- | src/components/emoji_picker/emoji_picker.vue | 77 | ||||
| -rw-r--r-- | src/components/lists_edit/lists_edit.js | 4 | ||||
| -rw-r--r-- | src/components/rich_content/rich_content.scss | 6 | ||||
| -rw-r--r-- | src/components/settings_modal/tabs/data_import_export_tab.vue | 10 | ||||
| -rw-r--r-- | src/components/still-image/still-image.js | 3 | ||||
| -rw-r--r-- | src/components/still-image/still-image.vue | 1 | ||||
| -rw-r--r-- | src/i18n/en.json | 3 | ||||
| -rw-r--r-- | src/i18n/uk.json | 32 | ||||
| -rw-r--r-- | src/modules/announcements.js | 2 | ||||
| -rw-r--r-- | src/modules/instance.js | 36 |
17 files changed, 206 insertions, 107 deletions
diff --git a/src/boot/after_store.js b/src/boot/after_store.js index 7a4672b6..1fa9dd2a 100644 --- a/src/boot/after_store.js +++ b/src/boot/after_store.js @@ -1,6 +1,8 @@ import { createApp } from 'vue' import { createRouter, createWebHistory } from 'vue-router' import vClickOutside from 'click-outside-vue3' +import VueVirtualScroller from 'vue-virtual-scroller' +import 'vue-virtual-scroller/dist/vue-virtual-scroller.css' import { FontAwesomeIcon, FontAwesomeLayers } from '@fortawesome/vue-fontawesome' @@ -397,6 +399,7 @@ const afterStoreSetup = async ({ store, i18n }) => { app.use(vClickOutside) app.use(VBodyScrollLock) + app.use(VueVirtualScroller) app.component('FAIcon', FontAwesomeIcon) app.component('FALayers', FontAwesomeLayers) diff --git a/src/components/announcement/announcement.js b/src/components/announcement/announcement.js index c10c7d90..30254926 100644 --- a/src/components/announcement/announcement.js +++ b/src/components/announcement/announcement.js @@ -27,6 +27,9 @@ const Announcement = { ...mapState({ currentUser: state => state.users.currentUser }), + canEditAnnouncement () { + return this.currentUser && this.currentUser.privileges.includes('announcements_manage_announcements') + }, content () { return this.announcement.content }, diff --git a/src/components/announcement/announcement.vue b/src/components/announcement/announcement.vue index 5f64232a..c1b35981 100644 --- a/src/components/announcement/announcement.vue +++ b/src/components/announcement/announcement.vue @@ -45,14 +45,14 @@ {{ $t('announcements.mark_as_read_action') }} </button> <button - v-if="currentUser && currentUser.role === 'admin'" + v-if="canEditAnnouncement" class="btn button-default" @click="enterEditMode" > {{ $t('announcements.edit_action') }} </button> <button - v-if="currentUser && currentUser.role === 'admin'" + v-if="canEditAnnouncement" class="btn button-default" @click="deleteAnnouncement" > diff --git a/src/components/announcements_page/announcements_page.js b/src/components/announcements_page/announcements_page.js index 0bb4892e..8d1204d4 100644 --- a/src/components/announcements_page/announcements_page.js +++ b/src/components/announcements_page/announcements_page.js @@ -28,6 +28,9 @@ const AnnouncementsPage = { }), announcements () { return this.$store.state.announcements.announcements + }, + canPostAnnouncement () { + return this.currentUser && this.currentUser.privileges.includes('announcements_manage_announcements') } }, methods: { diff --git a/src/components/announcements_page/announcements_page.vue b/src/components/announcements_page/announcements_page.vue index b1489dec..59ed283d 100644 --- a/src/components/announcements_page/announcements_page.vue +++ b/src/components/announcements_page/announcements_page.vue @@ -7,7 +7,7 @@ </div> <div class="panel-body"> <section - v-if="currentUser && currentUser.role === 'admin'" + v-if="canPostAnnouncement" > <div class="post-form"> <div class="heading"> diff --git a/src/components/emoji_picker/emoji_picker.js b/src/components/emoji_picker/emoji_picker.js index dd5e5217..4a447905 100644 --- a/src/components/emoji_picker/emoji_picker.js +++ b/src/components/emoji_picker/emoji_picker.js @@ -3,7 +3,6 @@ import Checkbox from '../checkbox/checkbox.vue' import Popover from 'src/components/popover/popover.vue' import StillImage from '../still-image/still-image.vue' import { ensureFinalFallback } from '../../i18n/languages.js' -import lozad from 'lozad' import { library } from '@fortawesome/fontawesome-svg-core' import { faBoxOpen, @@ -19,7 +18,7 @@ import { faCode, faFlag } from '@fortawesome/free-solid-svg-icons' -import { debounce, trim } from 'lodash' +import { debounce, trim, chunk } from 'lodash' library.add( faBoxOpen, @@ -82,6 +81,17 @@ const filterByKeyword = (list, keyword = '', languages, nameLocalizer) => { return orderedEmojiList.flat() } +const getOffset = (elem) => { + const style = elem.style.transform + const res = /translateY\((\d+)px\)/.exec(style) + if (!res) { return 0 } + return res[1] +} + +const toHeaderId = id => { + return id.replace(/^row-\d+-/, '') +} + const EmojiPicker = { props: { enableStickerPicker: { @@ -102,7 +112,8 @@ const EmojiPicker = { contentLoaded: false, groupRefs: {}, emojiRefs: {}, - filteredEmojiGroups: [] + filteredEmojiGroups: [], + width: 0 } }, components: { @@ -125,9 +136,6 @@ const EmojiPicker = { setGroupRef (name) { return el => { this.groupRefs[name] = el } }, - setEmojiRef (name) { - return el => { this.emojiRefs[name] = el } - }, onPopoverShown () { this.$emit('show') }, @@ -147,18 +155,21 @@ const EmojiPicker = { } this.$emit('emoji', { insertion: value, keepOpen: this.keepOpen }) }, - onScroll (e) { - const target = (e && e.target) || this.$refs['emoji-groups'] - this.updateScrolledClass(target) - this.scrolledGroup(target) + onScroll (startIndex, endIndex, visibleStartIndex, visibleEndIndex) { + const target = this.$refs['emoji-groups'].$el + this.scrolledGroup(target, visibleStartIndex, visibleEndIndex) }, - scrolledGroup (target) { + scrolledGroup (target, start, end) { const top = target.scrollTop + 5 this.$nextTick(() => { - this.allEmojiGroups.forEach(group => { + this.emojiItems.slice(start, end + 1).forEach(group => { + const headerId = toHeaderId(group.id) const ref = this.groupRefs['group-' + group.id] - if (ref && ref.offsetTop <= top) { - this.activeGroup = group.id + if (!ref) { return } + const elem = ref.$el.parentElement + if (!elem) { return } + if (elem && getOffset(elem) <= top) { + this.activeGroup = headerId } }) this.scrollHeader() @@ -181,14 +192,10 @@ const EmojiPicker = { setScroll(right + margin - headerCont.clientWidth) } }, - highlight (key) { - const ref = this.groupRefs['group-' + key] - const top = ref.offsetTop + highlight (groupId) { this.setShowStickers(false) - this.activeGroup = key - this.$nextTick(() => { - this.$refs['emoji-groups'].scrollTop = top + 1 - }) + const indexInList = this.emojiItems.findIndex(k => k.id === groupId) + this.$refs['emoji-groups'].scrollToItem(indexInList) }, updateScrolledClass (target) { if (target.scrollTop <= 5) { @@ -208,43 +215,13 @@ const EmojiPicker = { filterByKeyword (list, keyword) { return filterByKeyword(list, keyword, this.languages, this.maybeLocalizedEmojiName) }, - initializeLazyLoad () { - this.destroyLazyLoad() - this.$nextTick(() => { - this.$lozad = lozad('.still-image.emoji-picker-emoji', { - load: el => { - const name = el.getAttribute('data-emoji-name') - const vn = this.emojiRefs[name] - if (!vn) { - return - } - - vn.loadLazy() - } - }) - this.$lozad.observe() - }) - }, - waitForDomAndInitializeLazyLoad () { - this.$nextTick(() => this.initializeLazyLoad()) - }, - destroyLazyLoad () { - if (this.$lozad) { - if (this.$lozad.observer) { - this.$lozad.observer.disconnect() - } - if (this.$lozad.mutationObserver) { - this.$lozad.mutationObserver.disconnect() - } - } - }, onShowing () { const oldContentLoaded = this.contentLoaded + this.recalculateItemPerRow() this.$nextTick(() => { this.$refs.search.focus() }) this.contentLoaded = true - this.waitForDomAndInitializeLazyLoad() this.filteredEmojiGroups = this.getFilteredEmojiGroups() if (!oldContentLoaded) { this.$nextTick(() => { @@ -261,6 +238,14 @@ const EmojiPicker = { emojis: this.filterByKeyword(group.emojis, trim(this.keyword)) })) .filter(group => group.emojis.length > 0) + }, + recalculateItemPerRow () { + this.$nextTick(() => { + if (!this.$refs['emoji-groups']) { + return + } + this.width = this.$refs['emoji-groups'].$el.offsetWidth + }) } }, watch: { @@ -269,14 +254,22 @@ const EmojiPicker = { this.debouncedHandleKeywordChange() }, allCustomGroups () { - this.waitForDomAndInitializeLazyLoad() this.filteredEmojiGroups = this.getFilteredEmojiGroups() } }, - destroyed () { - this.destroyLazyLoad() - }, computed: { + minItemSize () { + return this.emojiHeight + }, + emojiHeight () { + return 32 + 4 + }, + emojiWidth () { + return 32 + 4 + }, + itemPerRow () { + return this.width ? Math.floor(this.width / this.emojiWidth - 1) : 6 + }, activeGroupView () { return this.showingStickers ? '' : this.activeGroup }, @@ -287,7 +280,11 @@ const EmojiPicker = { return 0 }, allCustomGroups () { - return this.$store.getters.groupedCustomEmojis + const emojis = this.$store.getters.groupedCustomEmojis + if (emojis.unpacked) { + emojis.unpacked.text = this.$t('emoji.unpacked') + } + return emojis }, defaultGroup () { return Object.keys(this.allCustomGroups)[0] @@ -310,10 +307,20 @@ const EmojiPicker = { }, debouncedHandleKeywordChange () { return debounce(() => { - this.waitForDomAndInitializeLazyLoad() this.filteredEmojiGroups = this.getFilteredEmojiGroups() }, 500) }, + emojiItems () { + return this.filteredEmojiGroups.map(group => + chunk(group.emojis, this.itemPerRow) + .map((items, index) => ({ + ...group, + id: index === 0 ? group.id : `row-${index}-${group.id}`, + emojis: items, + isFirstRow: index === 0 + }))) + .reduce((a, c) => a.concat(c), []) + }, languages () { return ensureFinalFallback(this.$store.getters.mergedConfig.interfaceLanguage) }, diff --git a/src/components/emoji_picker/emoji_picker.scss b/src/components/emoji_picker/emoji_picker.scss index 53363ec1..dda12197 100644 --- a/src/components/emoji_picker/emoji_picker.scss +++ b/src/components/emoji_picker/emoji_picker.scss @@ -74,6 +74,7 @@ $emoji-picker-emoji-size: 32px; } .emoji-groups { + height: 100%; min-height: 200px; } diff --git a/src/components/emoji_picker/emoji_picker.vue b/src/components/emoji_picker/emoji_picker.vue index ff56d637..453ecdfc 100644 --- a/src/components/emoji_picker/emoji_picker.vue +++ b/src/components/emoji_picker/emoji_picker.vue @@ -74,45 +74,56 @@ @input="$event.target.composing = false" > </div> - <div + <DynamicScroller ref="emoji-groups" class="emoji-groups" :class="groupsScrolledClass" - @scroll="onScroll" + :min-item-size="minItemSize" + :items="emojiItems" + :emit-update="true" + @update="onScroll" + @visible="recalculateItemPerRow" > - <div - v-for="group in filteredEmojiGroups" - :key="group.id" - class="emoji-group" - > - <h6 + <template #default="{ item: group, index, active }"> + <DynamicScrollerItem :ref="setGroupRef('group-' + group.id)" - class="emoji-group-title" - > - {{ group.text }} - </h6> - <span - v-for="emoji in group.emojis" - :key="group.id + emoji.displayText" - :title="maybeLocalizedEmojiName(emoji)" - class="emoji-item" - @click.stop.prevent="onEmoji(emoji)" + :item="group" + :active="active" + :data-index="index" + :size-dependencies="[group.emojis.length]" > - <span - v-if="!emoji.imageUrl" - class="emoji-picker-emoji -unicode" - >{{ emoji.replacement }}</span> - <still-image - v-else - :ref="setEmojiRef(group.id + emoji.displayText)" - class="emoji-picker-emoji -custom" - :data-src="emoji.imageUrl" - :data-emoji-name="group.id + emoji.displayText" - /> - </span> - <span :ref="setGroupRef('group-end-' + group.id)" /> - </div> - </div> + <div + class="emoji-group" + > + <h6 + v-if="group.isFirstRow" + class="emoji-group-title" + > + {{ group.text }} + </h6> + <span + v-for="emoji in group.emojis" + :key="group.id + emoji.displayText" + :title="maybeLocalizedEmojiName(emoji)" + class="emoji-item" + @click.stop.prevent="onEmoji(emoji)" + > + <span + v-if="!emoji.imageUrl" + class="emoji-picker-emoji -unicode" + >{{ emoji.replacement }}</span> + <still-image + v-else + class="emoji-picker-emoji -custom" + loading="lazy" + :src="emoji.imageUrl" + :data-emoji-name="group.id + emoji.displayText" + /> + </span> + </div> + </DynamicScrollerItem> + </template> + </DynamicScroller> <div class="keep-open"> <Checkbox v-model="keepOpen"> {{ $t('emoji.keep_open') }} diff --git a/src/components/lists_edit/lists_edit.js b/src/components/lists_edit/lists_edit.js index c22d1323..c33659df 100644 --- a/src/components/lists_edit/lists_edit.js +++ b/src/components/lists_edit/lists_edit.js @@ -95,10 +95,10 @@ const ListsNew = { return this.addedUserIds.has(user.id) }, addUser (user) { - this.$store.dispatch('addListAccount', { accountId: this.user.id, listId: this.id }) + this.$store.dispatch('addListAccount', { accountId: user.id, listId: this.id }) }, removeUser (userId) { - this.$store.dispatch('removeListAccount', { accountId: this.user.id, listId: this.id }) + this.$store.dispatch('removeListAccount', { accountId: userId, listId: this.id }) }, onSearchLoading (results) { this.searchLoading = true diff --git a/src/components/rich_content/rich_content.scss b/src/components/rich_content/rich_content.scss index db08ef1e..52042314 100644 --- a/src/components/rich_content/rich_content.scss +++ b/src/components/rich_content/rich_content.scss @@ -1,7 +1,11 @@ +@import '../../variables'; + .RichContent { blockquote { - margin: 0.2em 0 0.2em 2em; + margin: 0.2em 0 0.2em 0.2em; font-style: italic; + border-left: 0.2em solid var(--faint, $fallback--faint); + padding-left: 1em; } pre { diff --git a/src/components/settings_modal/tabs/data_import_export_tab.vue b/src/components/settings_modal/tabs/data_import_export_tab.vue index e3b7f407..48356c9b 100644 --- a/src/components/settings_modal/tabs/data_import_export_tab.vue +++ b/src/components/settings_modal/tabs/data_import_export_tab.vue @@ -78,6 +78,16 @@ {{ $t('settings.download_backup') }} </a> <span + v-else-if="backup.state === 'running'" + > + {{ $tc('settings.backup_running', backup.processed_number, { number: backup.processed_number }) }} + </span> + <span + v-else-if="backup.state === 'failed'" + > + {{ $t('settings.backup_failed') }} + </span> + <span v-else > {{ $t('settings.backup_not_ready') }} diff --git a/src/components/still-image/still-image.js b/src/components/still-image/still-image.js index 200ef147..56fd2fd9 100644 --- a/src/components/still-image/still-image.js +++ b/src/components/still-image/still-image.js @@ -8,7 +8,8 @@ const StillImage = { 'alt', 'height', 'width', - 'dataSrc' + 'dataSrc', + 'loading' ], data () { return { diff --git a/src/components/still-image/still-image.vue b/src/components/still-image/still-image.vue index 633fb229..d015e138 100644 --- a/src/components/still-image/still-image.vue +++ b/src/components/still-image/still-image.vue @@ -17,6 +17,7 @@ :data-src="dataSrc" :src="realSrc" :referrerpolicy="referrerpolicy" + :loading="loading" @load="onLoad" @error="onError" > diff --git a/src/i18n/en.json b/src/i18n/en.json index 59ee1c17..1ee1147a 100644 --- a/src/i18n/en.json +++ b/src/i18n/en.json @@ -225,6 +225,7 @@ "search_emoji": "Search for an emoji", "add_emoji": "Insert emoji", "custom": "Custom emoji", + "unpacked": "Unpacked emoji", "unicode": "Unicode emoji", "unicode_groups": { "activities": "Activities", @@ -390,6 +391,8 @@ "account_backup_table_head": "Backup", "download_backup": "Download", "backup_not_ready": "This backup is not ready yet.", + "backup_running": "This backup is in progress, processed {number} record. | This backup is in progress, processed {number} records.", + "backup_failed": "This backup has failed.", "remove_backup": "Remove", "list_backups_error": "Error fetching backup list: {error}", "add_backup": "Create a new backup", diff --git a/src/i18n/uk.json b/src/i18n/uk.json index 99b49868..c781b1f6 100644 --- a/src/i18n/uk.json +++ b/src/i18n/uk.json @@ -188,7 +188,8 @@ "mobile_sidebar": "Ввімкнути бокову панель", "mobile_notifications": "Відкрити сповіщення (є непрочитані)", "mobile_notifications_close": "Закрити сповіщення", - "edit_nav_mobile": "Редагувати панель навігації" + "edit_nav_mobile": "Редагувати панель навігації", + "announcements": "Анонси" }, "media_modal": { "next": "Наступна", @@ -735,7 +736,10 @@ "hide_favorites_description": "Не показувати список моїх вподобань (люди все одно отримують сповіщення)", "third_column_mode": "Коли достатньо місця, показувати третю колонку, що містить", "user_popover_avatar_action_open": "Відкрити профіль", - "wordfilter": "Фільтр слів" + "wordfilter": "Фільтр слів", + "mention_links": "Посилання для згадування", + "user_profiles": "Профілі користувачів", + "notification_visibility_polls": "Закінчення опитувань, в яких ви проголосували" }, "selectable_list": { "select_all": "Вибрати все" @@ -956,7 +960,8 @@ "show_all_conversation_with_icon": "{icon} {text}", "plus_more": "+{number} більше", "thread_show_full_with_icon": "{icon} {text}", - "show_only_conversation_under_this": "Показати всі відповіді на цей допис" + "show_only_conversation_under_this": "Показати всі відповіді на цей допис", + "status_history": "Історія змін" }, "timeline": { "no_more_statuses": "Більше немає дописів", @@ -996,5 +1001,26 @@ "reported_statuses": "Дописи, на які подано скаргу:", "reporter": "Позивач:", "reported_user": "Відповідач:" + }, + "announcements": { + "delete_action": "Видалити", + "page_header": "Анонси", + "title": "Анонси", + "mark_as_read_action": "Позначити як прочитане", + "post_form_header": "Розмістити оголошення", + "post_placeholder": "Введіть текст вашого оголошення тут...", + "post_action": "Пост", + "post_error": "Помилка: {error}", + "close_error": "Закрити", + "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/announcements.js b/src/modules/announcements.js index e4d2d2b0..4504263b 100644 --- a/src/modules/announcements.js +++ b/src/modules/announcements.js @@ -49,7 +49,7 @@ const announcements = { } const currentUser = store.rootState.users.currentUser - const isAdmin = currentUser && currentUser.role === 'admin' + const isAdmin = currentUser && currentUser.privileges.includes('announcements_manage_announcements') const getAnnouncements = async () => { if (!isAdmin) { diff --git a/src/modules/instance.js b/src/modules/instance.js index 3b15e62e..8e8d13d3 100644 --- a/src/modules/instance.js +++ b/src/modules/instance.js @@ -181,15 +181,28 @@ const instance = { }, groupedCustomEmojis (state) { const packsOf = emoji => { - return emoji.tags + const packs = emoji.tags .filter(k => k.startsWith('pack:')) - .map(k => k.slice(5)) // remove 'pack:' prefix + .map(k => { + const packName = k.slice(5) // remove 'pack:' prefix + return { + id: `custom-${packName}`, + text: packName + } + }) + + if (!packs.length) { + return [{ + id: 'unpacked' + }] + } else { + return packs + } } return state.customEmoji .reduce((res, emoji) => { - packsOf(emoji).forEach(packName => { - const packId = `custom-${packName}` + packsOf(emoji).forEach(({ id: packId, text: packName }) => { if (!res[packId]) { res[packId] = ({ id: packId, @@ -290,9 +303,22 @@ const instance = { const lb = b.toLowerCase() return la > lb ? 1 : (la < lb ? -1 : 0) } + const noPackLast = (a, b) => { + const aNull = a === '' + const bNull = b === '' + if (aNull === bNull) { + return 0 + } else if (aNull && !bNull) { + return 1 + } else { + return -1 + } + } const byPackThenByName = (a, b) => { const packOf = emoji => (emoji.tags.filter(k => k.startsWith('pack:'))[0] || '').slice(5) - return caseInsensitiveStrCmp(packOf(a), packOf(b)) || caseInsensitiveStrCmp(a.displayText, b.displayText) + const packOfA = packOf(a) + const packOfB = packOf(b) + return noPackLast(packOfA, packOfB) || caseInsensitiveStrCmp(packOfA, packOfB) || caseInsensitiveStrCmp(a.displayText, b.displayText) } const emoji = Object.entries(values).map(([key, value]) => { |
