diff --git a/CHANGELOG.md b/CHANGELOG.md index 42554607..c011835c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Private mode support - Support for 'Move' type notifications - Pleroma AMOLED dark theme +- User level domain mutes, under User Settings -> Mutes +- Emoji reactions for statuses +- MRF keyword policy disclosure ### Changed - Captcha now resets on failed registrations - Notifications column now cleans itself up to optimize performance when tab is left open for a long time @@ -17,6 +20,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Single notifications left unread when hitting read on another device/tab - Registration fixed - Deactivation of remote accounts from frontend +- Fixed NSFW unhiding not working with videos when using one-click unhiding/displaying ## [1.1.7 and earlier] - 2019-12-14 ### Added diff --git a/package.json b/package.json index 38936b23..5c7fa31e 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,7 @@ "chromatism": "^3.0.0", "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", @@ -43,6 +44,7 @@ "@babel/plugin-transform-runtime": "^7.7.6", "@babel/preset-env": "^7.7.6", "@babel/register": "^7.7.4", + "@ungap/event-target": "^0.1.0", "@vue/babel-helper-vue-jsx-merge-props": "^1.0.0", "@vue/babel-plugin-transform-vue-jsx": "^1.1.2", "@vue/test-utils": "^1.0.0-beta.26", @@ -56,6 +58,7 @@ "connect-history-api-fallback": "^1.1.0", "cross-spawn": "^4.0.2", "css-loader": "^0.28.0", + "custom-event-polyfill": "^1.0.7", "eslint": "^5.16.0", "eslint-config-standard": "^12.0.0", "eslint-friendly-formatter": "^2.0.5", diff --git a/src/App.scss b/src/App.scss index 754ca62e..922e39b6 100644 --- a/src/App.scss +++ b/src/App.scss @@ -75,7 +75,7 @@ button { border-radius: $fallback--btnRadius; border-radius: var(--btnRadius, $fallback--btnRadius); cursor: pointer; - box-shadow: 0px 0px 2px 0px rgba(0, 0, 0, 1), 0px 1px 0px 0px rgba(255, 255, 255, 0.2) inset, 0px -1px 0px 0px rgba(0, 0, 0, 0.2) inset; + box-shadow: $fallback--buttonShadow; box-shadow: var(--buttonShadow); font-size: 14px; font-family: sans-serif; diff --git a/src/_variables.scss b/src/_variables.scss index e18101f0..30dc3e42 100644 --- a/src/_variables.scss +++ b/src/_variables.scss @@ -27,3 +27,5 @@ $fallback--tooltipRadius: 5px; $fallback--avatarRadius: 4px; $fallback--avatarAltRadius: 10px; $fallback--attachmentRadius: 10px; + +$fallback--buttonShadow: 0px 0px 2px 0px rgba(0, 0, 0, 1), 0px 1px 0px 0px rgba(255, 255, 255, 0.2) inset, 0px -1px 0px 0px rgba(0, 0, 0, 0.2) inset; diff --git a/src/boot/after_store.js b/src/boot/after_store.js index 228a0497..0bb1b2b4 100644 --- a/src/boot/after_store.js +++ b/src/boot/after_store.js @@ -185,12 +185,9 @@ const getAppSecret = async ({ store }) => { }) } -const resolveStaffAccounts = async ({ store, accounts }) => { - const backendInteractor = store.state.api.backendInteractor - let nicknames = accounts.map(uri => uri.split('/').pop()) - .map(id => backendInteractor.fetchUser({ id })) - nicknames = await Promise.all(nicknames) - +const resolveStaffAccounts = ({ store, accounts }) => { + const nicknames = accounts.map(uri => uri.split('/').pop()) + nicknames.map(nickname => store.dispatch('fetchUser', nickname)) store.dispatch('setInstanceOption', { name: 'staffAccounts', value: nicknames }) } @@ -236,7 +233,7 @@ const getNodeInfo = async ({ store }) => { }) const accounts = metadata.staffAccounts - await resolveStaffAccounts({ store, accounts }) + resolveStaffAccounts({ store, accounts }) } else { throw (res) } diff --git a/src/components/attachment/attachment.js b/src/components/attachment/attachment.js index 06b496b0..b832e10f 100644 --- a/src/components/attachment/attachment.js +++ b/src/components/attachment/attachment.js @@ -2,6 +2,7 @@ import StillImage from '../still-image/still-image.vue' import VideoAttachment from '../video_attachment/video_attachment.vue' import nsfwImage from '../../assets/nsfw.png' import fileTypeService from '../../services/file_type/file_type.service.js' +import { mapGetters } from 'vuex' const Attachment = { props: [ @@ -49,7 +50,8 @@ const Attachment = { }, fullwidth () { return this.type === 'html' || this.type === 'audio' - } + }, + ...mapGetters(['mergedConfig']) }, methods: { linkClicked ({ target }) { @@ -58,7 +60,7 @@ const Attachment = { } }, openModal (event) { - const modalTypes = this.$store.getters.mergedConfig.playVideosInModal + const modalTypes = this.mergedConfig.playVideosInModal ? ['image', 'video'] : ['image'] if (fileTypeService.fileMatchesSomeType(modalTypes, this.attachment) || @@ -71,7 +73,10 @@ const Attachment = { } }, toggleHidden (event) { - if (this.$store.getters.mergedConfig.useOneClickNsfw && !this.showHidden) { + if ( + (this.mergedConfig.useOneClickNsfw && !this.showHidden) && + (this.type !== 'video' || this.mergedConfig.playVideosInModal) + ) { this.openModal(event) return } diff --git a/src/components/conversation/conversation.js b/src/components/conversation/conversation.js index ff99300a..428d973e 100644 --- a/src/components/conversation/conversation.js +++ b/src/components/conversation/conversation.js @@ -159,6 +159,7 @@ const conversation = { if (!id) return this.highlight = id this.$store.dispatch('fetchFavsAndRepeats', id) + this.$store.dispatch('fetchEmojiReactionsBy', id) }, getHighlight () { return this.isExpanded ? this.highlight : null diff --git a/src/components/domain_mute_card/domain_mute_card.js b/src/components/domain_mute_card/domain_mute_card.js new file mode 100644 index 00000000..c8e838ba --- /dev/null +++ b/src/components/domain_mute_card/domain_mute_card.js @@ -0,0 +1,15 @@ +import ProgressButton from '../progress_button/progress_button.vue' + +const DomainMuteCard = { + props: ['domain'], + components: { + ProgressButton + }, + methods: { + unmuteDomain () { + return this.$store.dispatch('unmuteDomain', this.domain) + } + } +} + +export default DomainMuteCard diff --git a/src/components/domain_mute_card/domain_mute_card.vue b/src/components/domain_mute_card/domain_mute_card.vue new file mode 100644 index 00000000..567d81c5 --- /dev/null +++ b/src/components/domain_mute_card/domain_mute_card.vue @@ -0,0 +1,38 @@ + + + + + diff --git a/src/components/emoji_reactions/emoji_reactions.js b/src/components/emoji_reactions/emoji_reactions.js new file mode 100644 index 00000000..b799ac9a --- /dev/null +++ b/src/components/emoji_reactions/emoji_reactions.js @@ -0,0 +1,72 @@ +import UserAvatar from '../user_avatar/user_avatar.vue' + +const EMOJI_REACTION_COUNT_CUTOFF = 12 + +const EmojiReactions = { + name: 'EmojiReactions', + components: { + UserAvatar + }, + props: ['status'], + data: () => ({ + showAll: false, + popperOptions: { + modifiers: { + preventOverflow: { padding: { top: 50 }, boundariesElement: 'viewport' } + } + } + }), + computed: { + tooManyReactions () { + return this.status.emoji_reactions.length > EMOJI_REACTION_COUNT_CUTOFF + }, + emojiReactions () { + return this.showAll + ? this.status.emoji_reactions + : this.status.emoji_reactions.slice(0, EMOJI_REACTION_COUNT_CUTOFF) + }, + showMoreString () { + return `+${this.status.emoji_reactions.length - EMOJI_REACTION_COUNT_CUTOFF}` + }, + accountsForEmoji () { + return this.status.emoji_reactions.reduce((acc, reaction) => { + acc[reaction.name] = reaction.accounts || [] + return acc + }, {}) + }, + loggedIn () { + return !!this.$store.state.users.currentUser + } + }, + methods: { + toggleShowAll () { + this.showAll = !this.showAll + }, + reactedWith (emoji) { + return this.status.emoji_reactions.find(r => r.name === emoji).me + }, + fetchEmojiReactionsByIfMissing () { + const hasNoAccounts = this.status.emoji_reactions.find(r => !r.accounts) + if (hasNoAccounts) { + this.$store.dispatch('fetchEmojiReactionsBy', this.status.id) + } + }, + reactWith (emoji) { + this.$store.dispatch('reactWithEmoji', { id: this.status.id, emoji }) + }, + unreact (emoji) { + this.$store.dispatch('unreactWithEmoji', { id: this.status.id, emoji }) + }, + emojiOnClick (emoji, event) { + if (!this.loggedIn) return + + if (this.reactedWith(emoji)) { + this.unreact(emoji) + } else { + this.reactWith(emoji) + } + } + } +} + +export default EmojiReactions diff --git a/src/components/emoji_reactions/emoji_reactions.vue b/src/components/emoji_reactions/emoji_reactions.vue new file mode 100644 index 00000000..e5b6d9f5 --- /dev/null +++ b/src/components/emoji_reactions/emoji_reactions.vue @@ -0,0 +1,136 @@ + + + + diff --git a/src/components/interactions/interactions.js b/src/components/interactions/interactions.js index cc31ff20..7fe5e76d 100644 --- a/src/components/interactions/interactions.js +++ b/src/components/interactions/interactions.js @@ -10,6 +10,7 @@ const tabModeDict = { const Interactions = { data () { return { + allowFollowingMove: this.$store.state.users.currentUser.allow_following_move, filterMode: tabModeDict['mentions'] } }, diff --git a/src/components/interactions/interactions.vue b/src/components/interactions/interactions.vue index a2e252ab..57d5d87c 100644 --- a/src/components/interactions/interactions.vue +++ b/src/components/interactions/interactions.vue @@ -22,6 +22,7 @@ :label="$t('interactions.follows')" /> diff --git a/src/components/mrf_transparency_panel/mrf_transparency_panel.js b/src/components/mrf_transparency_panel/mrf_transparency_panel.js index 6a1baec8..a0b600d2 100644 --- a/src/components/mrf_transparency_panel/mrf_transparency_panel.js +++ b/src/components/mrf_transparency_panel/mrf_transparency_panel.js @@ -11,7 +11,10 @@ const MRFTransparencyPanel = { rejectInstances: state => get(state, 'instance.federationPolicy.mrf_simple.reject', []), ftlRemovalInstances: state => get(state, 'instance.federationPolicy.mrf_simple.federated_timeline_removal', []), mediaNsfwInstances: state => get(state, 'instance.federationPolicy.mrf_simple.media_nsfw', []), - mediaRemovalInstances: state => get(state, 'instance.federationPolicy.mrf_simple.media_removal', []) + mediaRemovalInstances: state => get(state, 'instance.federationPolicy.mrf_simple.media_removal', []), + keywordsFtlRemoval: state => get(state, 'instance.federationPolicy.mrf_keyword.federated_timeline_removal', []), + keywordsReject: state => get(state, 'instance.federationPolicy.mrf_keyword.reject', []), + keywordsReplace: state => get(state, 'instance.federationPolicy.mrf_keyword.replace', []) }), hasInstanceSpecificPolicies () { return this.quarantineInstances.length || @@ -20,6 +23,11 @@ const MRFTransparencyPanel = { this.ftlRemovalInstances.length || this.mediaNsfwInstances.length || this.mediaRemovalInstances.length + }, + hasKeywordPolicies () { + return this.keywordsFtlRemoval.length || + this.keywordsReject.length || + this.keywordsReplace.length } } } diff --git a/src/components/mrf_transparency_panel/mrf_transparency_panel.vue b/src/components/mrf_transparency_panel/mrf_transparency_panel.vue index d6495dc6..8038e587 100644 --- a/src/components/mrf_transparency_panel/mrf_transparency_panel.vue +++ b/src/components/mrf_transparency_panel/mrf_transparency_panel.vue @@ -109,6 +109,49 @@ /> + +

+ {{ $t("about.mrf.keyword.keyword_policies") }} +

+ +
+

{{ $t("about.mrf.keyword.ftl_removal") }}

+ + +
+ +
+

{{ $t("about.mrf.keyword.reject") }}

+ + +
+ +
+

{{ $t("about.mrf.keyword.replace") }}

+ + +
diff --git a/src/components/nav_panel/nav_panel.js b/src/components/nav_panel/nav_panel.js index d9268585..8f7edb7f 100644 --- a/src/components/nav_panel/nav_panel.js +++ b/src/components/nav_panel/nav_panel.js @@ -3,7 +3,7 @@ import { mapState } from 'vuex' const NavPanel = { created () { if (this.currentUser && this.currentUser.locked) { - this.$store.dispatch('startFetchingFollowRequest') + this.$store.dispatch('startFetchingFollowRequests') } }, computed: mapState({ diff --git a/src/components/nav_panel/nav_panel.vue b/src/components/nav_panel/nav_panel.vue index 034259d9..0f3296eb 100644 --- a/src/components/nav_panel/nav_panel.vue +++ b/src/components/nav_panel/nav_panel.vue @@ -33,7 +33,7 @@ {{ $t("nav.public_tl") }} -
  • +
  • {{ $t("nav.twkn") }} diff --git a/src/components/notification/notification.vue b/src/components/notification/notification.vue index 16124e50..411c0271 100644 --- a/src/components/notification/notification.vue +++ b/src/components/notification/notification.vue @@ -78,6 +78,13 @@ {{ $t('notifications.migrated_to') }} + + + + {{ notification.emoji }} + + +
    r.name === emoji) + if (existingReaction && existingReaction.me) { + this.$store.dispatch('unreactWithEmoji', { id: this.status.id, emoji }) + } else { + this.$store.dispatch('reactWithEmoji', { id: this.status.id, emoji }) + } + this.closeReactionSelect() + } + }, + computed: { + commonEmojis () { + return ['❤️', '😠', '👀', '😂', '🔥'] + }, + emojis () { + if (this.filterWord !== '') { + return this.$store.state.instance.emoji.filter(emoji => emoji.displayText.includes(this.filterWord)) + } + return this.$store.state.instance.emoji || [] + }, + ...mapGetters(['mergedConfig']) + } +} + +export default ReactButton diff --git a/src/components/react_button/react_button.vue b/src/components/react_button/react_button.vue new file mode 100644 index 00000000..fb43ebaf --- /dev/null +++ b/src/components/react_button/react_button.vue @@ -0,0 +1,113 @@ + + + + + diff --git a/src/components/settings/settings.vue b/src/components/settings/settings.vue index 34d20e42..fd44c575 100644 --- a/src/components/settings/settings.vue +++ b/src/components/settings/settings.vue @@ -97,6 +97,11 @@ {{ $t('settings.virtual_scrolling') }}
  • +
  • + + {{ $t('settings.emoji_reactions_on_timeline') }} + +
  • @@ -333,6 +338,11 @@ {{ $t('settings.notification_visibility_moves') }} +
  • + + {{ $t('settings.notification_visibility_emoji_reactions') }} + +
  • diff --git a/src/components/side_drawer/side_drawer.js b/src/components/side_drawer/side_drawer.js index 2534eb8f..2181ecc7 100644 --- a/src/components/side_drawer/side_drawer.js +++ b/src/components/side_drawer/side_drawer.js @@ -12,7 +12,7 @@ const SideDrawer = { this.closeGesture = GestureService.swipeGesture(GestureService.DIRECTION_LEFT, this.toggleDrawer) if (this.currentUser && this.currentUser.locked) { - this.$store.dispatch('startFetchingFollowRequest') + this.$store.dispatch('startFetchingFollowRequests') } }, components: { UserCard }, diff --git a/src/components/side_drawer/side_drawer.vue b/src/components/side_drawer/side_drawer.vue index 3fba9058..28637afc 100644 --- a/src/components/side_drawer/side_drawer.vue +++ b/src/components/side_drawer/side_drawer.vue @@ -88,7 +88,7 @@
  • diff --git a/src/components/staff_panel/staff_panel.js b/src/components/staff_panel/staff_panel.js index 93e950ad..4f98fff6 100644 --- a/src/components/staff_panel/staff_panel.js +++ b/src/components/staff_panel/staff_panel.js @@ -1,3 +1,4 @@ +import map from 'lodash/map' import BasicUserCard from '../basic_user_card/basic_user_card.vue' const StaffPanel = { @@ -6,7 +7,7 @@ const StaffPanel = { }, computed: { staffAccounts () { - return this.$store.state.instance.staffAccounts + return map(this.$store.state.instance.staffAccounts, nickname => this.$store.getters.findUser(nickname)).filter(_ => _) } } } diff --git a/src/components/status/status.js b/src/components/status/status.js index 6941b8ec..da109a8f 100644 --- a/src/components/status/status.js +++ b/src/components/status/status.js @@ -1,5 +1,6 @@ import Attachment from '../attachment/attachment.vue' import FavoriteButton from '../favorite_button/favorite_button.vue' +import ReactButton from '../react_button/react_button.vue' import RetweetButton from '../retweet_button/retweet_button.vue' import Poll from '../poll/poll.vue' import ExtraButtons from '../extra_buttons/extra_buttons.vue' @@ -11,6 +12,7 @@ import LinkPreview from '../link-preview/link-preview.vue' import AvatarList from '../avatar_list/avatar_list.vue' import Timeago from '../timeago/timeago.vue' import StatusPopover from '../status_popover/status_popover.vue' +import EmojiReactions from '../emoji_reactions/emoji_reactions.vue' import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator' import fileType from 'src/services/file_type/file_type.service' import { processHtml } from 'src/services/tiny_post_html_processor/tiny_post_html_processor.service.js' @@ -255,6 +257,16 @@ const Status = { file => !fileType.fileMatchesSomeType(this.galleryTypes, file) ) }, + hasImageAttachments () { + return this.status.attachments.some( + file => fileType.fileType(file.mimetype) === 'image' + ) + }, + hasVideoAttachments () { + return this.status.attachments.some( + file => fileType.fileType(file.mimetype) === 'video' + ) + }, maxThumbnails () { return this.mergedConfig.maxThumbnails }, @@ -320,6 +332,7 @@ const Status = { components: { Attachment, FavoriteButton, + ReactButton, RetweetButton, ExtraButtons, PostStatusForm, @@ -330,7 +343,8 @@ const Status = { LinkPreview, AvatarList, Timeago, - StatusPopover + StatusPopover, + EmojiReactions }, methods: { visibilityIcon (visibility) { diff --git a/src/components/status/status.vue b/src/components/status/status.vue index d291e762..83f07dac 100644 --- a/src/components/status/status.vue +++ b/src/components/status/status.vue @@ -277,7 +277,21 @@ href="#" class="cw-status-hider" @click.prevent="toggleShowMore" - >{{ $t("general.show_more") }} + > + {{ $t("general.show_more") }} + + + + + +
    + $store.dispatch('fetchDomainMutes'), + select: (props, $store) => get($store.state.users.currentUser, 'domainMutes', []), + childPropName: 'items' +})(SelectableList) + const UserSettings = { data () { return { @@ -48,6 +55,7 @@ const UserSettings = { showRole: this.$store.state.users.currentUser.show_role, role: this.$store.state.users.currentUser.role, discoverable: this.$store.state.users.currentUser.discoverable, + allowFollowingMove: this.$store.state.users.currentUser.allow_following_move, pickAvatarBtnVisible: true, bannerUploading: false, backgroundUploading: false, @@ -67,7 +75,8 @@ const UserSettings = { changedPassword: false, changePasswordError: false, activeTab: 'profile', - notificationSettings: this.$store.state.users.currentUser.notification_settings + notificationSettings: this.$store.state.users.currentUser.notification_settings, + newDomainToMute: '' } }, created () { @@ -80,10 +89,12 @@ const UserSettings = { ImageCropper, BlockList, MuteList, + DomainMuteList, EmojiInput, Autosuggest, BlockCard, MuteCard, + DomainMuteCard, ProgressButton, Importer, Exporter, @@ -152,6 +163,7 @@ const UserSettings = { hide_follows: this.hideFollows, hide_followers: this.hideFollowers, discoverable: this.discoverable, + allow_following_move: this.allowFollowingMove, hide_follows_count: this.hideFollowsCount, hide_followers_count: this.hideFollowersCount, show_role: this.showRole @@ -297,7 +309,7 @@ const UserSettings = { newPassword: this.changePasswordInputs[1], newPasswordConfirmation: this.changePasswordInputs[2] } - this.$store.state.api.backendInteractor.changePassword({ params }) + this.$store.state.api.backendInteractor.changePassword(params) .then((res) => { if (res.status === 'success') { this.changedPassword = true @@ -314,7 +326,7 @@ const UserSettings = { email: this.newEmail, password: this.changeEmailPassword } - this.$store.state.api.backendInteractor.changeEmail({ params }) + this.$store.state.api.backendInteractor.changeEmail(params) .then((res) => { if (res.status === 'success') { this.changedEmail = true @@ -365,6 +377,13 @@ const UserSettings = { unmuteUsers (ids) { return this.$store.dispatch('unmuteUsers', ids) }, + unmuteDomains (domains) { + return this.$store.dispatch('unmuteDomains', domains) + }, + muteDomain () { + return this.$store.dispatch('muteDomain', this.newDomainToMute) + .then(() => { this.newDomainToMute = '' }) + }, identity (value) { return value } diff --git a/src/components/user_settings/user_settings.vue b/src/components/user_settings/user_settings.vue index 3f1982a6..8b2336b4 100644 --- a/src/components/user_settings/user_settings.vue +++ b/src/components/user_settings/user_settings.vue @@ -90,9 +90,7 @@

    - + {{ $t('settings.hide_followers_description') }}

    @@ -104,6 +102,11 @@ {{ $t('settings.hide_followers_count_description') }}

    +

    + + {{ $t('settings.allow_following_move') }} + +