diff --git a/CHANGELOG.md b/CHANGELOG.md index 1467f133..feabbf06 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,28 +4,38 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ## [Unreleased] ### Changed +- Greentext now has separate color slot for it - Removed the use of with_move parameters when fetching notifications +- Push notifications now are the same as normal notfication, and are localized. ### Fixed +- Weird bug related to post being sent seemingly after pasting with keyboard (hopefully) - Multiple issues with muted statuses/notifications ## [Unreleased patch] ### Add - Added private notifications option for push notifications - 'Copy link' button for statuses (in the ellipsis menu) +- Autocomplete domains from list of known instances +- 'Bot' settings option and badge ### Changed - Registration page no longer requires email if the server is configured not to require it - Change heart to thumbs up in reaction picker - Close the media modal on navigation events - Add colons to the emoji alt text, to make them copyable +- Add better visual indication for drag-and-drop for files ### Fixed +- Custom Emoji will display in poll options now. - Status ellipsis menu closes properly when selecting certain options - Cropped images look correct in Chrome - Newlines in the muted words settings work again - Clicking on non-latin hashtags won't open a new window - Uploading and drag-dropping multiple files works correctly now. +- Subject field now appears disabled when posting +- Fix status ellipsis menu being cut off in notifications column +- Fixed autocomplete sometimes not returning the right user when there's already some results ## [2.0.3] - 2020-05-02 ### Fixed diff --git a/package.json b/package.json index 4d68cc6e..c0665f6e 100644 --- a/package.json +++ b/package.json @@ -22,12 +22,9 @@ "cropperjs": "^1.4.3", "diff": "^3.0.1", "escape-html": "^1.0.3", - "karma-mocha-reporter": "^2.2.1", "localforage": "^1.5.0", - "object-path": "^0.11.3", "phoenix": "^1.3.0", "portal-vue": "^2.1.4", - "sanitize-html": "^1.13.0", "v-click-outside": "^2.1.1", "vue": "^2.6.11", "vue-chat-scroll": "^1.2.1", @@ -35,10 +32,10 @@ "vue-router": "^3.0.1", "vue-template-compiler": "^2.6.11", "vuelidate": "^0.7.4", - "vuex": "^3.0.1", - "whatwg-fetch": "^2.0.3" + "vuex": "^3.0.1" }, "devDependencies": { + "karma-mocha-reporter": "^2.2.1", "@babel/core": "^7.7.5", "@babel/plugin-transform-runtime": "^7.7.6", "@babel/preset-env": "^7.7.6", diff --git a/src/components/account_actions/account_actions.vue b/src/components/account_actions/account_actions.vue index 744b77d5..029e7096 100644 --- a/src/components/account_actions/account_actions.vue +++ b/src/components/account_actions/account_actions.vue @@ -3,6 +3,7 @@ <Popover trigger="click" placement="bottom" + :bound-to="{ x: 'container' }" > <div slot="content" diff --git a/src/components/domain_mute_card/domain_mute_card.js b/src/components/domain_mute_card/domain_mute_card.js index c8e838ba..f234dcb0 100644 --- a/src/components/domain_mute_card/domain_mute_card.js +++ b/src/components/domain_mute_card/domain_mute_card.js @@ -5,9 +5,20 @@ const DomainMuteCard = { components: { ProgressButton }, + computed: { + user () { + return this.$store.state.users.currentUser + }, + muted () { + return this.user.domainMutes.includes(this.domain) + } + }, methods: { unmuteDomain () { return this.$store.dispatch('unmuteDomain', this.domain) + }, + muteDomain () { + return this.$store.dispatch('muteDomain', this.domain) } } } diff --git a/src/components/domain_mute_card/domain_mute_card.vue b/src/components/domain_mute_card/domain_mute_card.vue index 567d81c5..97aee243 100644 --- a/src/components/domain_mute_card/domain_mute_card.vue +++ b/src/components/domain_mute_card/domain_mute_card.vue @@ -4,6 +4,7 @@ {{ domain }} </div> <ProgressButton + v-if="muted" :click="unmuteDomain" class="btn btn-default" > @@ -12,6 +13,16 @@ {{ $t('domain_mute_card.unmute_progress') }} </template> </ProgressButton> + <ProgressButton + v-else + :click="muteDomain" + class="btn btn-default" + > + {{ $t('domain_mute_card.mute') }} + <template slot="progress"> + {{ $t('domain_mute_card.mute_progress') }} + </template> + </ProgressButton> </div> </template> @@ -34,5 +45,9 @@ button { width: 10em; } + + .autosuggest-results & { + padding-left: 1em; + } } </style> diff --git a/src/components/emoji_input/suggestor.js b/src/components/emoji_input/suggestor.js index 15a71eff..8330345b 100644 --- a/src/components/emoji_input/suggestor.js +++ b/src/components/emoji_input/suggestor.js @@ -13,7 +13,7 @@ import { debounce } from 'lodash' const debounceUserSearch = debounce((data, input) => { data.updateUsersList(input) -}, 500, { leading: true, trailing: false }) +}, 500) export default data => input => { const firstChar = input[0] @@ -97,8 +97,8 @@ export const suggestUsers = data => input => { replacement: '@' + screen_name + ' ' })) - // BE search users if there are no matches - if (newUsers.length === 0 && data.updateUsersList) { + // BE search users to get more comprehensive results + if (data.updateUsersList) { debounceUserSearch(data, noPrefix) } return newUsers diff --git a/src/components/extra_buttons/extra_buttons.vue b/src/components/extra_buttons/extra_buttons.vue index bca93ea7..68db6fd8 100644 --- a/src/components/extra_buttons/extra_buttons.vue +++ b/src/components/extra_buttons/extra_buttons.vue @@ -3,6 +3,7 @@ trigger="click" placement="top" class="extra-button-popover" + :bound-to="{ x: 'container' }" > <div slot="content" diff --git a/src/components/gallery/gallery.vue b/src/components/gallery/gallery.vue index 7abc2161..1ffa7b3c 100644 --- a/src/components/gallery/gallery.vue +++ b/src/components/gallery/gallery.vue @@ -78,6 +78,7 @@ video, canvas { object-fit: contain; + height: 100%; } } diff --git a/src/components/media_upload/media_upload.js b/src/components/media_upload/media_upload.js index 5849b065..fbb2d03d 100644 --- a/src/components/media_upload/media_upload.js +++ b/src/components/media_upload/media_upload.js @@ -45,20 +45,6 @@ const mediaUpload = { this.$emit('all-uploaded') } }, - fileDrop (e) { - if (e.dataTransfer.files.length > 0) { - e.preventDefault() // allow dropping text like before - this.multiUpload(e.dataTransfer.files) - } - }, - fileDrag (e) { - let types = e.dataTransfer.types - if (types.contains('Files')) { - e.dataTransfer.dropEffect = 'copy' - } else { - e.dataTransfer.dropEffect = 'none' - } - }, clearFile () { this.uploadReady = false this.$nextTick(() => { diff --git a/src/components/media_upload/media_upload.vue b/src/components/media_upload/media_upload.vue index 0fc305ac..5e31730b 100644 --- a/src/components/media_upload/media_upload.vue +++ b/src/components/media_upload/media_upload.vue @@ -1,10 +1,5 @@ <template> - <div - class="media-upload" - @drop.prevent - @dragover.prevent="fileDrag" - @drop="fileDrop" - > + <div class="media-upload"> <label class="label" :title="$t('tool_tip.media_upload')" diff --git a/src/components/notifications/notifications.scss b/src/components/notifications/notifications.scss index b675af5a..20797cf9 100644 --- a/src/components/notifications/notifications.scss +++ b/src/components/notifications/notifications.scss @@ -54,25 +54,20 @@ flex-wrap: nowrap; padding: 0.6em; min-width: 0; + .avatar-container { width: 32px; height: 32px; } - .status-el { - .status { - padding: 0.25em 0; - color: $fallback--faint; - color: var(--faint, $fallback--faint); - a { - color: var(--faintLink); - } - .status-content a { - color: var(--postFaintLink); - } + + .status-body { + color: $fallback--faint; + color: var(--faint, $fallback--faint); + a { + color: var(--faintLink); } - padding: 0; - .media-body { - margin: 0; + .status-content a { + color: var(--postFaintLink); } } } diff --git a/src/components/poll/poll.vue b/src/components/poll/poll.vue index 56e91cca..adbb0555 100644 --- a/src/components/poll/poll.vue +++ b/src/components/poll/poll.vue @@ -17,7 +17,7 @@ <span class="result-percentage"> {{ percentageForOption(option.votes_count) }}% </span> - <span>{{ option.title }}</span> + <span v-html="option.title_html"></span> </div> <div class="result-fill" diff --git a/src/components/popover/popover.js b/src/components/popover/popover.js index 5881d266..a40a9195 100644 --- a/src/components/popover/popover.js +++ b/src/components/popover/popover.js @@ -1,4 +1,3 @@ - const Popover = { name: 'Popover', props: { @@ -10,6 +9,9 @@ const Popover = { // 'container' for using offsetParent as boundaries for either axis // or 'viewport' boundTo: Object, + // Takes a selector to use as a replacement for the parent container + // for getting boundaries for x an y axis + boundToSelector: String, // Takes a top/bottom/left/right object, how much space to leave // between boundary and popover element margin: Object, @@ -27,6 +29,10 @@ const Popover = { } }, methods: { + containerBoundingClientRect () { + const container = this.boundToSelector ? this.$el.closest(this.boundToSelector) : this.$el.offsetParent + return container.getBoundingClientRect() + }, updateStyles () { if (this.hidden) { this.styles = { @@ -45,7 +51,8 @@ const Popover = { // Minor optimization, don't call a slow reflow call if we don't have to const parentBounds = this.boundTo && (this.boundTo.x === 'container' || this.boundTo.y === 'container') && - this.$el.offsetParent.getBoundingClientRect() + this.containerBoundingClientRect() + const margin = this.margin || {} // What are the screen bounds for the popover? Viewport vs container diff --git a/src/components/post_status_form/post_status_form.js b/src/components/post_status_form/post_status_form.js index 6164caa0..9027566f 100644 --- a/src/components/post_status_form/post_status_form.js +++ b/src/components/post_status_form/post_status_form.js @@ -82,7 +82,9 @@ const PostStatusForm = { contentType }, caret: 0, - pollFormVisible: false + pollFormVisible: false, + showDropIcon: 'hide', + dropStopTimeout: null } }, computed: { @@ -248,13 +250,27 @@ const PostStatusForm = { } }, fileDrop (e) { - if (e.dataTransfer.files.length > 0) { + if (e.dataTransfer && e.dataTransfer.types.includes('Files')) { e.preventDefault() // allow dropping text like before this.dropFiles = e.dataTransfer.files + clearTimeout(this.dropStopTimeout) + this.showDropIcon = 'hide' } }, + fileDragStop (e) { + // The false-setting is done with delay because just using leave-events + // directly caused unwanted flickering, this is not perfect either but + // much less noticable. + clearTimeout(this.dropStopTimeout) + this.showDropIcon = 'fade' + this.dropStopTimeout = setTimeout(() => (this.showDropIcon = 'hide'), 500) + }, fileDrag (e) { e.dataTransfer.dropEffect = 'copy' + if (e.dataTransfer && e.dataTransfer.types.includes('Files')) { + clearTimeout(this.dropStopTimeout) + this.showDropIcon = 'show' + } }, onEmojiInputInput (e) { this.$nextTick(() => { diff --git a/src/components/post_status_form/post_status_form.vue b/src/components/post_status_form/post_status_form.vue index 5629ceac..e3d8d087 100644 --- a/src/components/post_status_form/post_status_form.vue +++ b/src/components/post_status_form/post_status_form.vue @@ -6,7 +6,15 @@ <form autocomplete="off" @submit.prevent="postStatus(newStatus)" + @dragover.prevent="fileDrag" > + <div + v-show="showDropIcon !== 'hide'" + :style="{ animation: showDropIcon === 'show' ? 'fade-in 0.25s' : 'fade-out 0.5s' }" + class="drop-indicator icon-upload" + @dragleave="fileDragStop" + @drop.stop="fileDrop" + /> <div class="form-group"> <i18n v-if="!$store.state.users.currentUser.locked && newStatus.visibility == 'private'" @@ -73,6 +81,7 @@ v-model="newStatus.spoilerText" type="text" :placeholder="$t('post_status.content_warning')" + :disabled="posting" class="form-post-subject" > </EmojiInput> @@ -96,9 +105,7 @@ :disabled="posting" class="form-post-body" @keydown.meta.enter="postStatus(newStatus)" - @keyup.ctrl.enter="postStatus(newStatus)" - @drop="fileDrop" - @dragover.prevent="fileDrag" + @keydown.ctrl.enter="postStatus(newStatus)" @input="resize" @compositionupdate="resize" @paste="paste" @@ -447,7 +454,8 @@ form { display: flex; flex-direction: column; - padding: 0.6em; + margin: 0.6em; + position: relative; } .form-group { @@ -505,5 +513,35 @@ cursor: pointer; z-index: 4; } + + @keyframes fade-in { + from { opacity: 0; } + to { opacity: 0.6; } + } + + @keyframes fade-out { + from { opacity: 0.6; } + to { opacity: 0; } + } + + .drop-indicator { + position: absolute; + z-index: 1; + width: 100%; + height: 100%; + font-size: 5em; + display: flex; + align-items: center; + justify-content: center; + opacity: 0.6; + color: $fallback--text; + color: var(--text, $fallback--text); + background-color: $fallback--bg; + background-color: var(--bg, $fallback--bg); + border-radius: $fallback--tooltipRadius; + border-radius: var(--tooltipRadius, $fallback--tooltipRadius); + border: 2px dashed $fallback--text; + border: 2px dashed var(--text, $fallback--text); + } } </style> diff --git a/src/components/post_status_modal/post_status_modal.js b/src/components/post_status_modal/post_status_modal.js index be945400..b44354db 100644 --- a/src/components/post_status_modal/post_status_modal.js +++ b/src/components/post_status_modal/post_status_modal.js @@ -13,6 +13,9 @@ const PostStatusModal = { } }, computed: { + isLoggedIn () { + return !!this.$store.state.users.currentUser + }, modalActivated () { return this.$store.state.postStatus.modalActivated }, diff --git a/src/components/post_status_modal/post_status_modal.vue b/src/components/post_status_modal/post_status_modal.vue index 07c58f74..dbcd321e 100644 --- a/src/components/post_status_modal/post_status_modal.vue +++ b/src/components/post_status_modal/post_status_modal.vue @@ -1,5 +1,6 @@ <template> <Modal + v-if="isLoggedIn && !resettingForm" :is-open="modalActivated" class="post-form-modal-view" @backdropClicked="closeModal" diff --git a/src/components/settings_modal/tabs/mutes_and_blocks_tab.js b/src/components/settings_modal/tabs/mutes_and_blocks_tab.js index b0043dbb..40a87b81 100644 --- a/src/components/settings_modal/tabs/mutes_and_blocks_tab.js +++ b/src/components/settings_modal/tabs/mutes_and_blocks_tab.js @@ -32,12 +32,12 @@ const DomainMuteList = withSubscription({ const MutesAndBlocks = { data () { return { - activeTab: 'profile', - newDomainToMute: '' + activeTab: 'profile' } }, created () { this.$store.dispatch('fetchTokens') + this.$store.dispatch('getKnownDomains') }, components: { TabSwitcher, @@ -51,6 +51,14 @@ const MutesAndBlocks = { Autosuggest, Checkbox }, + computed: { + knownDomains () { + return this.$store.state.instance.knownDomains + }, + user () { + return this.$store.state.users.currentUser + } + }, methods: { importFollows (file) { return this.$store.state.api.backendInteractor.importFollows({ file }) @@ -86,13 +94,13 @@ const MutesAndBlocks = { filterUnblockedUsers (userIds) { return reject(userIds, (userId) => { const relationship = this.$store.getters.relationship(this.userId) - return relationship.blocking || userId === this.$store.state.users.currentUser.id + return relationship.blocking || userId === this.user.id }) }, filterUnMutedUsers (userIds) { return reject(userIds, (userId) => { const relationship = this.$store.getters.relationship(this.userId) - return relationship.muting || userId === this.$store.state.users.currentUser.id + return relationship.muting || userId === this.user.id }) }, queryUserIds (query) { @@ -111,12 +119,16 @@ const MutesAndBlocks = { unmuteUsers (ids) { return this.$store.dispatch('unmuteUsers', ids) }, + filterUnMutedDomains (urls) { + return urls.filter(url => !this.user.domainMutes.includes(url)) + }, + queryKnownDomains (query) { + return new Promise((resolve, reject) => { + resolve(this.knownDomains.filter(url => url.toLowerCase().includes(query))) + }) + }, unmuteDomains (domains) { return this.$store.dispatch('unmuteDomains', domains) - }, - muteDomain () { - return this.$store.dispatch('muteDomain', this.newDomainToMute) - .then(() => { this.newDomainToMute = '' }) } } } diff --git a/src/components/settings_modal/tabs/mutes_and_blocks_tab.vue b/src/components/settings_modal/tabs/mutes_and_blocks_tab.vue index 6884b7be..5a1cf2c0 100644 --- a/src/components/settings_modal/tabs/mutes_and_blocks_tab.vue +++ b/src/components/settings_modal/tabs/mutes_and_blocks_tab.vue @@ -119,21 +119,16 @@ <div :label="$t('settings.domain_mutes')"> <div class="domain-mute-form"> - <input - v-model="newDomainToMute" + <Autosuggest + :filter="filterUnMutedDomains" + :query="queryKnownDomains" :placeholder="$t('settings.type_domains_to_mute')" - type="text" - @keyup.enter="muteDomain" > - <ProgressButton - class="btn btn-default domain-mute-button" - :click="muteDomain" - > - {{ $t('domain_mute_card.mute') }} - <template slot="progress"> - {{ $t('domain_mute_card.mute_progress') }} - </template> - </ProgressButton> + <DomainMuteCard + slot-scope="row" + :domain="row.item" + /> + </Autosuggest> </div> <DomainMuteList :refresh="true" diff --git a/src/components/settings_modal/tabs/profile_tab.js b/src/components/settings_modal/tabs/profile_tab.js index 0874e0c8..e6db802d 100644 --- a/src/components/settings_modal/tabs/profile_tab.js +++ b/src/components/settings_modal/tabs/profile_tab.js @@ -25,6 +25,7 @@ const ProfileTab = { showRole: this.$store.state.users.currentUser.show_role, role: this.$store.state.users.currentUser.role, discoverable: this.$store.state.users.currentUser.discoverable, + bot: this.$store.state.users.currentUser.bot, allowFollowingMove: this.$store.state.users.currentUser.allow_following_move, pickAvatarBtnVisible: true, bannerUploading: false, @@ -94,6 +95,7 @@ const ProfileTab = { hide_follows: this.hideFollows, hide_followers: this.hideFollowers, discoverable: this.discoverable, + bot: this.bot, allow_following_move: this.allowFollowingMove, hide_follows_count: this.hideFollowsCount, hide_followers_count: this.hideFollowersCount, diff --git a/src/components/settings_modal/tabs/profile_tab.vue b/src/components/settings_modal/tabs/profile_tab.vue index 20a68f7d..0f9210a6 100644 --- a/src/components/settings_modal/tabs/profile_tab.vue +++ b/src/components/settings_modal/tabs/profile_tab.vue @@ -143,6 +143,11 @@ {{ $t("settings.profile_fields.add_field") }} </a> </div> + <p> + <Checkbox v-model="bot"> + {{ $t('settings.bot') }} + </Checkbox> + </p> <button :disabled="newName && newName.length === 0" class="btn btn-default" diff --git a/src/components/settings_modal/tabs/theme_tab/theme_tab.vue b/src/components/settings_modal/tabs/theme_tab/theme_tab.vue index fcfad23b..d14f854c 100644 --- a/src/components/settings_modal/tabs/theme_tab/theme_tab.vue +++ b/src/components/settings_modal/tabs/theme_tab/theme_tab.vue @@ -256,6 +256,13 @@ :label="$t('settings.links')" /> <ContrastRatio :contrast="previewContrast.postLink" /> + <ColorInput + v-model="postGreentextColorLocal" + name="postGreentextColor" + :fallback="previewTheme.colors.cGreen" + :label="$t('settings.greentext')" + /> + <ContrastRatio :contrast="previewContrast.postGreentext" /> <h4>{{ $t('settings.style.advanced_colors.alert') }}</h4> <ColorInput v-model="alertErrorColorLocal" diff --git a/src/components/status/status.vue b/src/components/status/status.vue index 336f912a..7ec29b28 100644 --- a/src/components/status/status.vue +++ b/src/components/status/status.vue @@ -418,7 +418,7 @@ $status-margin: 0.75em; max-width: 85%; font-weight: bold; - img { + img.emoji { width: 14px; height: 14px; vertical-align: middle; diff --git a/src/components/status_content/status_content.vue b/src/components/status_content/status_content.vue index 8c2e8749..efc2485e 100644 --- a/src/components/status_content/status_content.vue +++ b/src/components/status_content/status_content.vue @@ -164,23 +164,23 @@ $status-margin: 0.75em; word-break: break-all; } + img, video { + max-width: 100%; + max-height: 400px; + vertical-align: middle; + object-fit: contain; + + &.emoji { + width: 32px; + height: 32px; + } + } + .status-content { font-family: var(--postFont, sans-serif); line-height: 1.4em; white-space: pre-wrap; - img, video { - max-width: 100%; - max-height: 400px; - vertical-align: middle; - object-fit: contain; - - &.emoji { - width: 32px; - height: 32px; - } - } - blockquote { margin: 0.2em 0 0.2em 2em; font-style: italic; @@ -226,7 +226,7 @@ $status-margin: 0.75em; .greentext { color: $fallback--cGreen; - color: var(--cGreen, $fallback--cGreen); + color: var(--postGreentext, $fallback--cGreen); } .timeline :not(.panel-disabled) > { diff --git a/src/components/still-image/still-image.vue b/src/components/still-image/still-image.vue index 08af26f6..f2ddeb7b 100644 --- a/src/components/still-image/still-image.vue +++ b/src/components/still-image/still-image.vue @@ -23,13 +23,6 @@ <style lang="scss"> @import '../../_variables.scss'; -.contain-fit { - .still-image { - img { - height: 100%; - } - } -} .still-image { position: relative; @@ -38,6 +31,7 @@ width: 100%; height: 100%; display: flex; + align-items: center; &:hover canvas { display: none; @@ -45,8 +39,8 @@ img { width: 100%; + min-height: 100%; object-fit: contain; - align-self: center; } &.animated { diff --git a/src/components/user_card/user_card.vue b/src/components/user_card/user_card.vue index c4a5ce9d..9529d7f6 100644 --- a/src/components/user_card/user_card.vue +++ b/src/components/user_card/user_card.vue @@ -70,10 +70,20 @@ > @{{ user.screen_name }} </router-link> - <span - v-if="!hideBio && !!visibleRole" - class="alert staff" - >{{ visibleRole }}</span> + <template v-if="!hideBio"> + <span + v-if="!!visibleRole" + class="alert user-role" + > + {{ visibleRole }} + </span> + <span + v-if="user.bot" + class="alert user-role" + > + bot + </span> + </template> <span v-if="user.locked"><i class="icon icon-lock" /></span> <span v-if="!mergedConfig.hideUserStats && !hideBio" @@ -458,7 +468,7 @@ color: var(--text, $fallback--text); } - .staff { + .user-role { flex: none; text-transform: capitalize; color: $fallback--text; diff --git a/src/components/user_profile/user_profile.js b/src/components/user_profile/user_profile.js index 95760bf8..201727d4 100644 --- a/src/components/user_profile/user_profile.js +++ b/src/components/user_profile/user_profile.js @@ -124,6 +124,14 @@ const UserProfile = { onTabSwitch (tab) { this.tab = tab this.$router.replace({ query: { tab } }) + }, + linkClicked ({ target }) { + if (target.tagName === 'SPAN') { + target = target.parentNode + } + if (target.tagName === 'A') { + window.open(target.href, '_blank') + } } }, watch: { diff --git a/src/components/user_profile/user_profile.vue b/src/components/user_profile/user_profile.vue index 1871d46c..361a3b5c 100644 --- a/src/components/user_profile/user_profile.vue +++ b/src/components/user_profile/user_profile.vue @@ -11,6 +11,31 @@ :allow-zooming-avatar="true" rounded="top" /> + <div + v-if="user.fields_html && user.fields_html.length > 0" + class="user-profile-fields" + > + <dl + v-for="(field, index) in user.fields_html" + :key="index" + class="user-profile-field" + > + <!-- eslint-disable vue/no-v-html --> + <dt + :title="user.fields_text[index].name" + class="user-profile-field-name" + @click.prevent="linkClicked" + v-html="field.name" + /> + <dd + :title="user.fields_text[index].value" + class="user-profile-field-value" + @click.prevent="linkClicked" + v-html="field.value" + /> + <!-- eslint-enable vue/no-v-html --> + </dl> + </div> <tab-switcher :active-tab="tab" :render-only-focused="true" @@ -108,11 +133,60 @@ <script src="./user_profile.js"></script> <style lang="scss"> +@import '../../_variables.scss'; .user-profile { flex: 2; flex-basis: 500px; + .user-profile-fields { + margin: 0 0.5em; + img { + object-fit: contain; + vertical-align: middle; + max-width: 100%; + max-height: 400px; + + &.emoji { + width: 18px; + height: 18px; + } + } + + .user-profile-field { + display: flex; + margin: 0.25em auto; + max-width: 32em; + border: 1px solid var(--border, $fallback--border); + border-radius: $fallback--inputRadius; + border-radius: var(--inputRadius, $fallback--inputRadius); + + .user-profile-field-name { + flex: 0 1 30%; + font-weight: 500; + text-align: right; + color: var(--lightText); + min-width: 120px; + border-right: 1px solid var(--border, $fallback--border); + } + + .user-profile-field-value { + flex: 1 1 70%; + color: var(--text); + margin: 0 0 0 0.25em; + } + + .user-profile-field-name, .user-profile-field-value { + line-height: 18px; + text-overflow: ellipsis; + white-space: nowrap; + overflow: hidden; + padding: 0.5em 1.5em; + box-sizing: border-box; + } + } + } + .userlist-placeholder { display: flex; justify-content: center; diff --git a/src/i18n/en.json b/src/i18n/en.json index ca49514e..2840904f 100644 --- a/src/i18n/en.json +++ b/src/i18n/en.json @@ -266,6 +266,7 @@ "block_import_error": "Error importing blocks", "blocks_imported": "Blocks imported! Processing them will take a while.", "blocks_tab": "Blocks", + "bot": "This is a bot account", "btnRadius": "Buttons", "cBlue": "Blue (Reply, follow)", "cGreen": "Green (Retweet)", @@ -407,7 +408,7 @@ "theme_help_v2_1": "You can also override certain component's colors and opacity by toggling the checkbox, use \"Clear all\" button to clear all overrides.", "theme_help_v2_2": "Icons underneath some entries are background/text contrast indicators, hover over for detailed info. Please keep in mind that when using transparency contrast indicators show the worst possible case.", "tooltipRadius": "Tooltips/alerts", - "type_domains_to_mute": "Type in domains to mute", + "type_domains_to_mute": "Search domains to mute", "upload_a_photo": "Upload a photo", "user_settings": "User Settings", "values": { diff --git a/src/i18n/it.json b/src/i18n/it.json index 360c72aa..6c8be351 100644 --- a/src/i18n/it.json +++ b/src/i18n/it.json @@ -12,7 +12,12 @@ "disable": "Disabilita", "enable": "Abilita", "confirm": "Conferma", - "verify": "Verifica" + "verify": "Verifica", + "peek": "Anteprima", + "close": "Chiudi", + "retry": "Riprova", + "error_retry": "Per favore, riprova", + "loading": "Carico…" }, "nav": { "mentions": "Menzioni", @@ -212,7 +217,63 @@ }, "common": { "opacity": "Opacità", - "color": "Colore" + "color": "Colore", + "contrast": { + "context": { + "text": "per il testo", + "18pt": "per il testo grande (oltre 17pt)" + }, + "level": { + "bad": "non soddisfa le linee guida di alcun livello", + "aaa": "soddisfa le linee guida di livello AAA (ottimo)", + "aa": "soddisfa le linee guida di livello AA (sufficiente)" + }, + "hint": "Il rapporto di contrasto è {ratio}, e {level} {context}" + } + }, + "advanced_colors": { + "badge": "Sfondo medaglie", + "post": "Messaggi / Biografie", + "alert_neutral": "Neutro", + "alert_warning": "Attenzione", + "alert_error": "Errore", + "alert": "Sfondo degli avvertimenti", + "_tab_label": "Avanzate", + "tabs": "Etichette", + "disabled": "Disabilitato", + "selectedMenu": "Voce menù selezionata", + "selectedPost": "Messaggio selezionato", + "pressed": "Premuto", + "highlight": "Elementi evidenziati", + "icons": "Icone", + "poll": "Grafico sondaggi", + "underlay": "Sottostante", + "faint_text": "Testo sbiadito", + "inputs": "Campi d'immissione", + "buttons": "Pulsanti", + "borders": "Bordi", + "top_bar": "Barra superiore", + "panel_header": "Titolo pannello", + "badge_notification": "Notifica", + "popover": "Suggerimenti, menù, sbalzi" + }, + "common_colors": { + "rgbo": "Icone, accenti, medaglie", + "foreground_hint": "Seleziona l'etichetta \"Avanzate\" per controlli più fini", + "main": "Colori comuni", + "_tab_label": "Comuni" + }, + "shadows": { + "inset": "Includi", + "spread": "Spandi", + "blur": "Sfoca", + "shadow_id": "Ombra numero {value}", + "override": "Sostituisci", + "component": "Componente", + "_tab_label": "Luci ed ombre" + }, + "radii": { + "_tab_label": "Raggio" } }, "enable_web_push_notifications": "Abilita notifiche web push", @@ -229,7 +290,7 @@ "notifications": "Notifiche", "greentext": "Frecce da meme", "upload_a_photo": "Carica un'immagine", - "type_domains_to_mute": "Inserisci domini da zittire", + "type_domains_to_mute": "Cerca domini da zittire", "theme_help_v2_2": "Le icone dietro alcuni elementi sono indicatori del contrasto fra testo e sfondo, passaci sopra col puntatore per ulteriori informazioni. Se si usano delle trasparenze, questi indicatori mostrano il peggior caso possibile.", "theme_help_v2_1": "Puoi anche forzare colore ed opacità di alcuni elementi selezionando la casella. Usa il pulsante \"Azzera\" per azzerare tutte le forzature.", "useStreamingApiWarning": "(Sconsigliato, sperimentale, può saltare messaggi)", @@ -273,7 +334,8 @@ "accent": "Accento", "emoji_reactions_on_timeline": "Mostra emoji di reazione sulle sequenze", "pad_emoji": "Affianca spazi agli emoji inseriti tramite selettore", - "notification_blocks": "Bloccando un utente non riceverai più le sue notifiche né lo seguirai più." + "notification_blocks": "Bloccando un utente non riceverai più le sue notifiche né lo seguirai più.", + "mutes_and_blocks": "Zittiti e bloccati" }, "timeline": { "error_fetching": "Errore nell'aggiornamento", diff --git a/src/i18n/ru.json b/src/i18n/ru.json index 69b22618..aa78db26 100644 --- a/src/i18n/ru.json +++ b/src/i18n/ru.json @@ -130,6 +130,7 @@ "background": "Фон", "bio": "Описание", "btnRadius": "Кнопки", + "bot": "Это аккаунт бота", "cBlue": "Ответить, читать", "cGreen": "Повторить", "cOrange": "Нравится", @@ -456,9 +457,9 @@ }, "domain_mute_card": { "mute": "Игнорировать", - "mute_progress": "В процессе...", + "mute_progress": "В процессе…", "unmute": "Прекратить игнорирование", - "unmute_progress": "В процессе..." + "unmute_progress": "В процессе…" }, "exporter": { "export": "Экспорт", diff --git a/src/i18n/service_worker_messages.js b/src/i18n/service_worker_messages.js new file mode 100644 index 00000000..270ed043 --- /dev/null +++ b/src/i18n/service_worker_messages.js @@ -0,0 +1,35 @@ +/* eslint-disable import/no-webpack-loader-syntax */ +// This module exports only the notification part of the i18n, +// which is useful for the service worker + +const messages = { + ar: require('../lib/notification-i18n-loader.js!./ar.json'), + ca: require('../lib/notification-i18n-loader.js!./ca.json'), + cs: require('../lib/notification-i18n-loader.js!./cs.json'), + de: require('../lib/notification-i18n-loader.js!./de.json'), + eo: require('../lib/notification-i18n-loader.js!./eo.json'), + es: require('../lib/notification-i18n-loader.js!./es.json'), + et: require('../lib/notification-i18n-loader.js!./et.json'), + eu: require('../lib/notification-i18n-loader.js!./eu.json'), + fi: require('../lib/notification-i18n-loader.js!./fi.json'), + fr: require('../lib/notification-i18n-loader.js!./fr.json'), + ga: require('../lib/notification-i18n-loader.js!./ga.json'), + he: require('../lib/notification-i18n-loader.js!./he.json'), + hu: require('../lib/notification-i18n-loader.js!./hu.json'), + it: require('../lib/notification-i18n-loader.js!./it.json'), + ja: require('../lib/notification-i18n-loader.js!./ja_pedantic.json'), + ja_easy: require('../lib/notification-i18n-loader.js!./ja_easy.json'), + ko: require('../lib/notification-i18n-loader.js!./ko.json'), + nb: require('../lib/notification-i18n-loader.js!./nb.json'), + nl: require('../lib/notification-i18n-loader.js!./nl.json'), + oc: require('../lib/notification-i18n-loader.js!./oc.json'), + pl: require('../lib/notification-i18n-loader.js!./pl.json'), + pt: require('../lib/notification-i18n-loader.js!./pt.json'), + ro: require('../lib/notification-i18n-loader.js!./ro.json'), + ru: require('../lib/notification-i18n-loader.js!./ru.json'), + te: require('../lib/notification-i18n-loader.js!./te.json'), + zh: require('../lib/notification-i18n-loader.js!./zh.json'), + en: require('../lib/notification-i18n-loader.js!./en.json') +} + +export default messages diff --git a/src/lib/notification-i18n-loader.js b/src/lib/notification-i18n-loader.js new file mode 100644 index 00000000..71f9156a --- /dev/null +++ b/src/lib/notification-i18n-loader.js @@ -0,0 +1,12 @@ +// This somewhat mysterious module will load a json string +// and then extract only the 'notifications' part. This is +// meant to be used to load the partial i18n we need for +// the service worker. +module.exports = function (source) { + var object = JSON.parse(source) + var smol = { + notifications: object.notifications || {} + } + + return JSON.stringify(smol) +} diff --git a/src/lib/persisted_state.js b/src/lib/persisted_state.js index cad7ea25..8ecb66a8 100644 --- a/src/lib/persisted_state.js +++ b/src/lib/persisted_state.js @@ -1,13 +1,12 @@ import merge from 'lodash.merge' -import objectPath from 'object-path' import localforage from 'localforage' -import { each } from 'lodash' +import { each, get, set } from 'lodash' let loaded = false const defaultReducer = (state, paths) => ( paths.length === 0 ? state : paths.reduce((substate, path) => { - objectPath.set(substate, path, objectPath.get(state, path)) + set(substate, path, get(state, path)) return substate }, {}) ) diff --git a/src/modules/instance.js b/src/modules/instance.js index da82eb01..ec5f4e54 100644 --- a/src/modules/instance.js +++ b/src/modules/instance.js @@ -1,6 +1,7 @@ import { set } from 'vue' import { getPreset, applyTheme } from '../services/style_setter/style_setter.js' import { CURRENT_VERSION } from '../services/theme_data/theme_data.service.js' +import apiService from '../services/api/api.service.js' import { instanceDefaultProperties } from './config.js' const defaultState = { @@ -48,6 +49,7 @@ const defaultState = { postFormats: [], restrictedNicknames: [], safeDM: true, + knownDomains: [], // Feature-set, apparently, not everything here is reported... chatAvailable: false, @@ -80,6 +82,9 @@ const instance = { if (typeof value !== 'undefined') { set(state, name, value) } + }, + setKnownDomains (state, domains) { + state.knownDomains = domains } }, getters: { @@ -182,6 +187,18 @@ const instance = { state.emojiFetched = true dispatch('getStaticEmoji') } + }, + + async getKnownDomains ({ commit, rootState }) { + try { + const result = await apiService.fetchKnownDomains({ + credentials: rootState.users.currentUser.credentials + }) + commit('setKnownDomains', result) + } catch (e) { + console.warn("Can't load known domains") + console.warn(e) + } } } } diff --git a/src/modules/statuses.js b/src/modules/statuses.js index 9a2e0df1..073b15f1 100644 --- a/src/modules/statuses.js +++ b/src/modules/statuses.js @@ -13,7 +13,7 @@ import { omitBy } from 'lodash' import { set } from 'vue' -import { isStatusNotification } from '../services/notification_utils/notification_utils.js' +import { isStatusNotification, prepareNotificationObject } from '../services/notification_utils/notification_utils.js' import apiService from '../services/api/api.service.js' import { muteWordHits } from '../services/status_parser/status_parser.js' @@ -344,42 +344,7 @@ const addNewNotifications = (state, { dispatch, notifications, older, visibleNot state.notifications.idStore[notification.id] = notification if ('Notification' in window && window.Notification.permission === 'granted') { - const notifObj = {} - const status = notification.status - const title = notification.from_profile.name - notifObj.icon = notification.from_profile.profile_image_url - let i18nString - switch (notification.type) { - case 'like': - i18nString = 'favorited_you' - break - case 'repeat': - i18nString = 'repeated_you' - break - case 'follow': - i18nString = 'followed_you' - break - case 'move': - i18nString = 'migrated_to' - break - case 'follow_request': - i18nString = 'follow_request' - break - } - - if (notification.type === 'pleroma:emoji_reaction') { - notifObj.body = rootGetters.i18n.t('notifications.reacted_with', [notification.emoji]) - } else if (i18nString) { - notifObj.body = rootGetters.i18n.t('notifications.' + i18nString) - } else if (isStatusNotification(notification.type)) { - notifObj.body = notification.status.text - } - - // Shows first attached non-nsfw image, if any. Should add configuration for this somehow... - if (status && status.attachments && status.attachments.length > 0 && !status.nsfw && - status.attachments[0].mimetype.startsWith('image/')) { - notifObj.image = status.attachments[0].url - } + const notifObj = prepareNotificationObject(notification, rootGetters.i18n) const reasonsToMuteNotif = ( notification.seen || @@ -393,7 +358,7 @@ const addNewNotifications = (state, { dispatch, notifications, older, visibleNot ) ) if (!reasonsToMuteNotif) { - let desktopNotification = new window.Notification(title, notifObj) + let desktopNotification = new window.Notification(notifObj.title, notifObj) // Chrome is known for not closing notifications automatically // according to MDN, anyway. setTimeout(desktopNotification.close.bind(desktopNotification), 5000) diff --git a/src/modules/users.js b/src/modules/users.js index fca01a56..68d02931 100644 --- a/src/modules/users.js +++ b/src/modules/users.js @@ -435,10 +435,10 @@ const users = { store.commit('setUserForNotification', notification) }) }, - searchUsers (store, { query }) { - return store.rootState.api.backendInteractor.searchUsers({ query }) + searchUsers ({ rootState, commit }, { query }) { + return rootState.api.backendInteractor.searchUsers({ query }) .then((users) => { - store.commit('addNewUsers', users) + commit('addNewUsers', users) return users }) }, diff --git a/src/services/api/api.service.js b/src/services/api/api.service.js index 9c7530a2..dfffc291 100644 --- a/src/services/api/api.service.js +++ b/src/services/api/api.service.js @@ -1,6 +1,5 @@ import { each, map, concat, last, get } from 'lodash' import { parseStatus, parseUser, parseNotification, parseAttachment } from '../entity_normalizer/entity_normalizer.service.js' -import 'whatwg-fetch' import { RegistrationError, StatusCodeError } from '../errors/errors' /* eslint-env browser */ @@ -75,6 +74,7 @@ const MASTODON_SEARCH_2 = `/api/v2/search` const MASTODON_USER_SEARCH_URL = '/api/v1/accounts/search' const MASTODON_DOMAIN_BLOCKS_URL = '/api/v1/domain_blocks' const MASTODON_STREAMING = '/api/v1/streaming' +const MASTODON_KNOWN_DOMAIN_LIST_URL = '/api/v1/instance/peers' const PLEROMA_EMOJI_REACTIONS_URL = id => `/api/v1/pleroma/statuses/${id}/reactions` const PLEROMA_EMOJI_REACT_URL = (id, emoji) => `/api/v1/pleroma/statuses/${id}/reactions/${emoji}` const PLEROMA_EMOJI_UNREACT_URL = (id, emoji) => `/api/v1/pleroma/statuses/${id}/reactions/${emoji}` @@ -995,6 +995,10 @@ const search2 = ({ credentials, q, resolve, limit, offset, following }) => { }) } +const fetchKnownDomains = ({ credentials }) => { + return promisedRequest({ url: MASTODON_KNOWN_DOMAIN_LIST_URL, credentials }) +} + const fetchDomainMutes = ({ credentials }) => { return promisedRequest({ url: MASTODON_DOMAIN_BLOCKS_URL, credentials }) } @@ -1193,6 +1197,7 @@ const apiService = { updateNotificationSettings, search2, searchUsers, + fetchKnownDomains, fetchDomainMutes, muteDomain, unmuteDomain diff --git a/src/services/entity_normalizer/entity_normalizer.service.js b/src/services/entity_normalizer/entity_normalizer.service.js index c7ed65a4..3bdb92f3 100644 --- a/src/services/entity_normalizer/entity_normalizer.service.js +++ b/src/services/entity_normalizer/entity_normalizer.service.js @@ -56,6 +56,12 @@ export const parseUser = (data) => { value: addEmojis(field.value, data.emojis) } }) + output.fields_text = data.fields.map(field => { + return { + name: unescape(field.name.replace(/<[^>]*>/g, '')), + value: unescape(field.value.replace(/<[^>]*>/g, '')) + } + }) // Utilize avatar_static for gif avatars? output.profile_image_url = data.avatar @@ -258,6 +264,12 @@ export const parseStatus = (data) => { output.summary_html = addEmojis(escape(data.spoiler_text), data.emojis) output.external_url = data.url output.poll = data.poll + if (output.poll) { + output.poll.options = (output.poll.options || []).map(field => ({ + ...field, + title_html: addEmojis(field.title, data.emojis) + })) + } output.pinned = data.pinned output.muted = data.muted } else { diff --git a/src/services/notification_utils/notification_utils.js b/src/services/notification_utils/notification_utils.js index eb479227..5cc19215 100644 --- a/src/services/notification_utils/notification_utils.js +++ b/src/services/notification_utils/notification_utils.js @@ -43,3 +43,47 @@ export const filteredNotificationsFromStore = (store, types) => { export const unseenNotificationsFromStore = store => filter(filteredNotificationsFromStore(store), ({ seen }) => !seen) + +export const prepareNotificationObject = (notification, i18n) => { + const notifObj = { + tag: notification.id + } + const status = notification.status + const title = notification.from_profile.name + notifObj.title = title + notifObj.icon = notification.from_profile.profile_image_url + let i18nString + switch (notification.type) { + case 'like': + i18nString = 'favorited_you' + break + case 'repeat': + i18nString = 'repeated_you' + break + case 'follow': + i18nString = 'followed_you' + break + case 'move': + i18nString = 'migrated_to' + break + case 'follow_request': + i18nString = 'follow_request' + break + } + + if (notification.type === 'pleroma:emoji_reaction') { + notifObj.body = i18n.t('notifications.reacted_with', [notification.emoji]) + } else if (i18nString) { + notifObj.body = i18n.t('notifications.' + i18nString) + } else if (isStatusNotification(notification.type)) { + notifObj.body = notification.status.text + } + + // Shows first attached non-nsfw image, if any. Should add configuration for this somehow... + if (status && status.attachments && status.attachments.length > 0 && !status.nsfw && + status.attachments[0].mimetype.startsWith('image/')) { + notifObj.image = status.attachments[0].url + } + + return notifObj +} diff --git a/src/services/status_parser/status_parser.js b/src/services/status_parser/status_parser.js index 3d517e3c..ed0f6d57 100644 --- a/src/services/status_parser/status_parser.js +++ b/src/services/status_parser/status_parser.js @@ -1,17 +1,4 @@ import { filter } from 'lodash' -import sanitize from 'sanitize-html' - -export const removeAttachmentLinks = (html) => { - return sanitize(html, { - allowedTags: false, - allowedAttributes: false, - exclusiveFilter: ({ tag, attribs }) => tag === 'a' && typeof attribs.class === 'string' && attribs.class.match(/attachment/) - }) -} - -export const parse = (html) => { - return removeAttachmentLinks(html) -} export const muteWordHits = (status, muteWords) => { const statusText = status.text.toLowerCase() @@ -22,5 +9,3 @@ export const muteWordHits = (status, muteWords) => { return hits } - -export default parse diff --git a/src/services/theme_data/pleromafe.js b/src/services/theme_data/pleromafe.js index 0c1fe543..b577cfab 100644 --- a/src/services/theme_data/pleromafe.js +++ b/src/services/theme_data/pleromafe.js @@ -356,6 +356,12 @@ export const SLOT_INHERITANCE = { textColor: 'preserve' }, + postGreentext: { + depends: ['cGreen'], + layer: 'bg', + textColor: 'preserve' + }, + border: { depends: ['fg'], opacity: 'border', diff --git a/src/sw.js b/src/sw.js index 6cecb3f3..f5e34dd6 100644 --- a/src/sw.js +++ b/src/sw.js @@ -1,6 +1,19 @@ /* eslint-env serviceworker */ import localForage from 'localforage' +import { parseNotification } from './services/entity_normalizer/entity_normalizer.service.js' +import { prepareNotificationObject } from './services/notification_utils/notification_utils.js' +import Vue from 'vue' +import VueI18n from 'vue-i18n' +import messages from './i18n/service_worker_messages.js' + +Vue.use(VueI18n) +const i18n = new VueI18n({ + // By default, use the browser locale, we will update it if neccessary + locale: 'en', + fallbackLocale: 'en', + messages +}) function isEnabled () { return localForage.getItem('vuex-lz') @@ -12,15 +25,33 @@ function getWindowClients () { .then((clientList) => clientList.filter(({ type }) => type === 'window')) } -self.addEventListener('push', (event) => { - if (event.data) { - event.waitUntil(isEnabled().then((isEnabled) => { - return isEnabled && getWindowClients().then((list) => { - const data = event.data.json() +const setLocale = async () => { + const state = await localForage.getItem('vuex-lz') + const locale = state.config.interfaceLanguage || 'en' + i18n.locale = locale +} - if (list.length === 0) return self.registration.showNotification(data.title, data) - }) - })) +const maybeShowNotification = async (event) => { + const enabled = await isEnabled() + const activeClients = await getWindowClients() + await setLocale() + if (enabled && (activeClients.length === 0)) { + const data = event.data.json() + + const url = `${self.registration.scope}api/v1/notifications/${data.notification_id}` + const notification = await fetch(url, { headers: { Authorization: 'Bearer ' + data.access_token } }) + const notificationJson = await notification.json() + const parsedNotification = parseNotification(notificationJson) + + const res = prepareNotificationObject(parsedNotification, i18n) + + self.registration.showNotification(res.title, res) + } +} + +self.addEventListener('push', async (event) => { + if (event.data) { + event.waitUntil(maybeShowNotification(event)) } }) diff --git a/static/terms-of-service.html b/static/terms-of-service.html index a6da539e..b2c66815 100644 --- a/static/terms-of-service.html +++ b/static/terms-of-service.html @@ -1,4 +1,9 @@ <h4>Terms of Service</h4> -<p>This is a placeholder ToS. Edit <code>"/static/terms-of-service.html"</code> to make it fit the needs of your instance.</p> +<p>This is the default placeholder ToS. You should copy it over to your static folder and edit it to fit the needs of your instance.</p> + +<p>To do so, place a file at <code>"/instance/static/terms-of-service.html"</code> in your + Pleroma install containing the real ToS for your instance.</p> +<p>See the <a href='https://docs.pleroma.social/backend/configuration/static_dir/'>Pleroma documentation</a> for more information.</p> +<br> <img src="/static/logo.png" style="display: block; margin: auto; max-width: 100%; height: 50px; object-fit: contain;" /> diff --git a/static/themes/redmond-xx-se.json b/static/themes/redmond-xx-se.json index 7a4a29da..24480d2c 100644 --- a/static/themes/redmond-xx-se.json +++ b/static/themes/redmond-xx-se.json @@ -286,7 +286,9 @@ "cGreen": "#008000", "cOrange": "#808000", "highlight": "--accent", - "selectedPost": "--bg,-10" + "selectedPost": "--bg,-10", + "selectedMenu": "--accent", + "selectedMenuPopover": "--accent" }, "radii": { "btn": "0", diff --git a/static/themes/redmond-xx.json b/static/themes/redmond-xx.json index ff95b1e0..cf9010fe 100644 --- a/static/themes/redmond-xx.json +++ b/static/themes/redmond-xx.json @@ -277,7 +277,9 @@ "cGreen": "#008000", "cOrange": "#808000", "highlight": "--accent", - "selectedPost": "--bg,-10" + "selectedPost": "--bg,-10", + "selectedMenu": "--accent", + "selectedMenuPopover": "--accent" }, "radii": { "btn": "0", diff --git a/static/themes/redmond-xxi.json b/static/themes/redmond-xxi.json index f788bdb8..7fdc4a6d 100644 --- a/static/themes/redmond-xxi.json +++ b/static/themes/redmond-xxi.json @@ -259,7 +259,9 @@ "cGreen": "#669966", "cOrange": "#cc6633", "highlight": "--accent", - "selectedPost": "--bg,-10" + "selectedPost": "--bg,-10", + "selectedMenu": "--accent", + "selectedMenuPopover": "--accent" }, "radii": { "btn": "0", diff --git a/test/unit/specs/services/entity_normalizer/entity_normalizer.spec.js b/test/unit/specs/services/entity_normalizer/entity_normalizer.spec.js index 166fce2b..ccb57942 100644 --- a/test/unit/specs/services/entity_normalizer/entity_normalizer.spec.js +++ b/test/unit/specs/services/entity_normalizer/entity_normalizer.spec.js @@ -290,6 +290,19 @@ describe('API Entities normalizer', () => { expect(field).to.have.property('value').that.contains('<img') }) + it('removes html tags from user profile fields', () => { + const user = makeMockUserMasto({ emojis: makeMockEmojiMasto(), fields: [{ name: 'user', value: '<a rel="me" href="https://example.com/@user">@user</a>' }] }) + + const parsedUser = parseUser(user) + + expect(parsedUser).to.have.property('fields_text').to.be.an('array') + + const field = parsedUser.fields_text[0] + + expect(field).to.have.property('name').that.equal('user') + expect(field).to.have.property('value').that.equal('@user') + }) + it('adds hide_follows and hide_followers user settings', () => { const user = makeMockUserMasto({ pleroma: { hide_followers: true, hide_follows: false, hide_followers_count: false, hide_follows_count: true } }) diff --git a/test/unit/specs/services/status_parser/status_parses.spec.js b/test/unit/specs/services/status_parser/status_parses.spec.js deleted file mode 100644 index 7afd5042..00000000 --- a/test/unit/specs/services/status_parser/status_parses.spec.js +++ /dev/null @@ -1,17 +0,0 @@ -import { removeAttachmentLinks } from '../../../../../src/services/status_parser/status_parser.js' - -const example = '<div class="status-content">@<a href="https://sealion.club/user/4" class="h-card mention" title="dewoo">dwmatiz</a> <a href="https://social.heldscal.la/file/3deb764ada10ce64a61b7a070b75dac45f86d2d5bf213bf18873da71d8714d86.png" title="https://social.heldscal.la/file/3deb764ada10ce64a61b7a070b75dac45f86d2d5bf213bf18873da71d8714d86.png" class="attachment" id="attachment-159853" rel="nofollow external">https://social.heldscal.la/attachment/159853</a></div>' - -describe('statusParser.removeAttachmentLinks', () => { - const exampleWithoutAttachmentLinks = '<div class="status-content">@<a href="https://sealion.club/user/4" class="h-card mention" title="dewoo">dwmatiz</a> </div>' - - it('removes attachment links', () => { - const parsed = removeAttachmentLinks(example) - expect(parsed).to.eql(exampleWithoutAttachmentLinks) - }) - - it('works when the class is empty', () => { - const parsed = removeAttachmentLinks('<a></a>') - expect(parsed).to.eql('<a></a>') - }) -}) diff --git a/yarn.lock b/yarn.lock index 61afa7ca..f05b00b1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1062,7 +1062,7 @@ array-union@^1.0.1: dependencies: array-uniq "^1.0.1" -array-uniq@^1.0.1, array-uniq@^1.0.2: +array-uniq@^1.0.1: version "1.0.3" resolved "https://registry.yarnpkg.com/array-uniq/-/array-uniq-1.0.3.tgz#af6ac877a25cc7f74e058894753858dfdb24fdb6" @@ -2545,7 +2545,7 @@ domain-browser@^1.1.1: version "1.2.0" resolved "https://registry.yarnpkg.com/domain-browser/-/domain-browser-1.2.0.tgz#3d31f50191a6749dd1375a7f522e823d42e54eda" -domelementtype@1, domelementtype@^1.3.0: +domelementtype@1: version "1.3.1" resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-1.3.1.tgz#d048c44b37b0d10a7f2a3d5fee3f4333d790481f" @@ -2559,12 +2559,6 @@ domhandler@2.1: dependencies: domelementtype "1" -domhandler@^2.3.0: - version "2.4.2" - resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-2.4.2.tgz#8805097e933d65e85546f726d60f5eb88b44f803" - dependencies: - domelementtype "1" - domutils@1.1: version "1.1.6" resolved "https://registry.yarnpkg.com/domutils/-/domutils-1.1.6.tgz#bddc3de099b9a2efacc51c623f28f416ecc57485" @@ -2578,13 +2572,6 @@ domutils@1.5.1: dom-serializer "0" domelementtype "1" -domutils@^1.5.1: - version "1.7.0" - resolved "https://registry.yarnpkg.com/domutils/-/domutils-1.7.0.tgz#56ea341e834e06e6748af7a1cb25da67ea9f8c2a" - dependencies: - dom-serializer "0" - domelementtype "1" - duplexer2@~0.1.4: version "0.1.4" resolved "https://registry.yarnpkg.com/duplexer2/-/duplexer2-0.1.4.tgz#8b12dab878c0d69e3e7891051662a32fc6bddcc1" @@ -2711,7 +2698,7 @@ ent@~2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/ent/-/ent-2.2.0.tgz#e964219325a21d05f44466a2f686ed6ce5f5dd1d" -entities@^1.1.1, entities@~1.1.1: +entities@~1.1.1: version "1.1.2" resolved "https://registry.yarnpkg.com/entities/-/entities-1.1.2.tgz#bdfa735299664dfafd34529ed4f8522a275fea56" @@ -3762,17 +3749,6 @@ html-webpack-plugin@^3.0.0, html-webpack-plugin@^3.2.0: toposort "^1.0.0" util.promisify "1.0.0" -htmlparser2@^3.10.0: - version "3.10.0" - resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-3.10.0.tgz#5f5e422dcf6119c0d983ed36260ce9ded0bee464" - dependencies: - domelementtype "^1.3.0" - domhandler "^2.3.0" - domutils "^1.5.1" - entities "^1.1.1" - inherits "^2.0.1" - readable-stream "^3.0.6" - htmlparser2@~3.3.0: version "3.3.0" resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-3.3.0.tgz#cc70d05a59f6542e43f0e685c982e14c924a9efe" @@ -4757,10 +4733,6 @@ lodash.clone@3.0.3: lodash._bindcallback "^3.0.0" lodash._isiterateecall "^3.0.0" -lodash.clonedeep@^4.5.0: - version "4.5.0" - resolved "https://registry.yarnpkg.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz#e23f3f9c4f8fbdde872529c1071857a086e5ccef" - lodash.create@3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/lodash.create/-/lodash.create-3.1.1.tgz#d7f2849f0dbda7e04682bb8cd72ab022461debe7" @@ -4780,10 +4752,6 @@ lodash.defaultsdeep@4.3.2: lodash.mergewith "^4.0.0" lodash.rest "^4.0.0" -lodash.escaperegexp@^4.1.2: - version "4.1.2" - resolved "https://registry.yarnpkg.com/lodash.escaperegexp/-/lodash.escaperegexp-4.1.2.tgz#64762c48618082518ac3df4ccf5d5886dae20347" - lodash.find@^3.2.1: version "3.2.1" resolved "https://registry.yarnpkg.com/lodash.find/-/lodash.find-3.2.1.tgz#046e319f3ace912ac6c9246c7f683c5ec07b36ad" @@ -4815,14 +4783,10 @@ lodash.isplainobject@^3.0.0, lodash.isplainobject@^3.2.0: lodash.isarguments "^3.0.0" lodash.keysin "^3.0.0" -lodash.isplainobject@^4.0.0, lodash.isplainobject@^4.0.6: +lodash.isplainobject@^4.0.0: version "4.0.6" resolved "https://registry.yarnpkg.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz#7c526a52d89b45c45cc690b88163be0497f550cb" -lodash.isstring@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/lodash.isstring/-/lodash.isstring-4.0.1.tgz#d527dfb5456eca7cc9bb95d5daeaf88ba54a5451" - lodash.istypedarray@^3.0.0: version "3.0.6" resolved "https://registry.yarnpkg.com/lodash.istypedarray/-/lodash.istypedarray-3.0.6.tgz#c9a477498607501d8e8494d283b87c39281cef62" @@ -4871,7 +4835,7 @@ lodash.merge@^3.3.2: lodash.keysin "^3.0.0" lodash.toplainobject "^3.0.0" -lodash.mergewith@^4.0.0, lodash.mergewith@^4.6.1: +lodash.mergewith@^4.0.0: version "4.6.1" resolved "https://registry.yarnpkg.com/lodash.mergewith/-/lodash.mergewith-4.6.1.tgz#639057e726c3afbdb3e7d42741caa8d6e4335927" @@ -5538,10 +5502,6 @@ object-keys@^1.0.11, object-keys@^1.0.12: version "1.1.1" resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e" -object-path@^0.11.3: - version "0.11.4" - resolved "https://registry.yarnpkg.com/object-path/-/object-path-0.11.4.tgz#370ae752fbf37de3ea70a861c23bba8915691949" - object-visit@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/object-visit/-/object-visit-1.0.1.tgz#f79c4493af0c5377b59fe39d395e41042dd045bb" @@ -6245,14 +6205,6 @@ postcss@^7.0.0: source-map "^0.6.1" supports-color "^6.1.0" -postcss@^7.0.5: - version "7.0.8" - resolved "https://registry.yarnpkg.com/postcss/-/postcss-7.0.8.tgz#2a3c5f2bdd00240cd0d0901fd998347c93d36696" - dependencies: - chalk "^2.4.2" - source-map "^0.6.1" - supports-color "^6.0.0" - prelude-ls@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54" @@ -6521,14 +6473,6 @@ readable-stream@1.1.x: isarray "0.0.1" string_decoder "~0.10.x" -readable-stream@^3.0.6: - version "3.1.1" - resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.1.1.tgz#ed6bbc6c5ba58b090039ff18ce670515795aeb06" - dependencies: - inherits "^2.0.3" - string_decoder "^1.1.1" - util-deprecate "^1.0.1" - readdirp@^2.2.1: version "2.2.1" resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-2.2.1.tgz#0e87622a3325aa33e892285caf8b4e846529a525" @@ -6839,21 +6783,6 @@ samsam@1.x, samsam@^1.1.3: version "1.3.0" resolved "https://registry.yarnpkg.com/samsam/-/samsam-1.3.0.tgz#8d1d9350e25622da30de3e44ba692b5221ab7c50" -sanitize-html@^1.13.0: - version "1.20.0" - resolved "https://registry.yarnpkg.com/sanitize-html/-/sanitize-html-1.20.0.tgz#9a602beb1c9faf960fb31f9890f61911cc4d9156" - dependencies: - chalk "^2.4.1" - htmlparser2 "^3.10.0" - lodash.clonedeep "^4.5.0" - lodash.escaperegexp "^4.1.2" - lodash.isplainobject "^4.0.6" - lodash.isstring "^4.0.1" - lodash.mergewith "^4.6.1" - postcss "^7.0.5" - srcset "^1.0.0" - xtend "^4.0.1" - "sass-loader@git://github.com/webpack-contrib/sass-loader": version "7.1.0" resolved "git://github.com/webpack-contrib/sass-loader#e279f2a129eee0bd0b624b5acd498f23a81ee35e" @@ -7225,13 +7154,6 @@ sprintf-js@~1.0.2: version "1.0.3" resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" -srcset@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/srcset/-/srcset-1.0.0.tgz#a5669de12b42f3b1d5e83ed03c71046fc48f41ef" - dependencies: - array-uniq "^1.0.2" - number-is-nan "^1.0.0" - sshpk@^1.7.0: version "1.16.0" resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.16.0.tgz#1d4963a2fbffe58050aa9084ca20be81741c07de" @@ -7331,7 +7253,7 @@ string-width@^3.0.0: is-fullwidth-code-point "^2.0.0" strip-ansi "^5.1.0" -string_decoder@^1.0.0, string_decoder@^1.1.1: +string_decoder@^1.0.0: version "1.2.0" resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.2.0.tgz#fe86e738b19544afe70469243b2a1ee9240eae8d" dependencies: @@ -7415,7 +7337,7 @@ supports-color@^5.3.0, supports-color@^5.4.0: dependencies: has-flag "^3.0.0" -supports-color@^6.0.0, supports-color@^6.1.0: +supports-color@^6.1.0: version "6.1.0" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-6.1.0.tgz#0764abc69c63d5ac842dd4867e8d025e880df8f3" dependencies: @@ -7780,7 +7702,7 @@ useragent@2.3.0: lru-cache "4.1.x" tmp "0.0.x" -util-deprecate@^1.0.1, util-deprecate@~1.0.1: +util-deprecate@~1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" @@ -8015,10 +7937,6 @@ webpack@^4.0.0: watchpack "^1.5.0" webpack-sources "^1.3.0" -whatwg-fetch@^2.0.3: - version "2.0.4" - resolved "https://registry.yarnpkg.com/whatwg-fetch/-/whatwg-fetch-2.0.4.tgz#dde6a5df315f9d39991aa17621853d720b85566f" - whet.extend@~0.9.9: version "0.9.9" resolved "https://registry.yarnpkg.com/whet.extend/-/whet.extend-0.9.9.tgz#f877d5bf648c97e5aa542fadc16d6a259b9c11a1" @@ -8090,7 +8008,7 @@ xregexp@2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/xregexp/-/xregexp-2.0.0.tgz#52a63e56ca0b84a7f3a5f3d61872f126ad7a5943" -xtend@^4.0.0, xtend@^4.0.1, xtend@~4.0.1: +xtend@^4.0.0, xtend@~4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.1.tgz#a5c6d532be656e23db820efb943a1f04998d63af"