Merge branch 'develop' into 'themes-accent'

# Conflicts:
#   src/components/emoji_reactions/emoji_reactions.vue
This commit is contained in:
HJ 2020-02-11 23:09:15 +00:00
commit 84ebae8ed3
28 changed files with 352 additions and 55 deletions

View file

@ -14,6 +14,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Pleroma AMOLED dark theme - Pleroma AMOLED dark theme
- User level domain mutes, under User Settings -> Mutes - User level domain mutes, under User Settings -> Mutes
- Emoji reactions for statuses - Emoji reactions for statuses
- MRF keyword policy disclosure
### Changed ### Changed
- theme engine update to 3 (themes v2.1 introduction) - theme engine update to 3 (themes v2.1 introduction)
- massive internal changes in theme engine - slowly away from "generate things separately with spaghetti code" towards "feed all data into single 'generateTheme' function and declare slot inheritance and all in a separate file" - massive internal changes in theme engine - slowly away from "generate things separately with spaghetti code" towards "feed all data into single 'generateTheme' function and declare slot inheritance and all in a separate file"

View file

@ -78,7 +78,7 @@ button {
border-radius: $fallback--btnRadius; border-radius: $fallback--btnRadius;
border-radius: var(--btnRadius, $fallback--btnRadius); border-radius: var(--btnRadius, $fallback--btnRadius);
cursor: pointer; 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); box-shadow: var(--buttonShadow);
font-size: 14px; font-size: 14px;
font-family: sans-serif; font-family: sans-serif;

View file

@ -27,3 +27,5 @@ $fallback--tooltipRadius: 5px;
$fallback--avatarRadius: 4px; $fallback--avatarRadius: 4px;
$fallback--avatarAltRadius: 10px; $fallback--avatarAltRadius: 10px;
$fallback--attachmentRadius: 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;

View file

@ -1,17 +1,55 @@
import UserAvatar from '../user_avatar/user_avatar.vue'
const EMOJI_REACTION_COUNT_CUTOFF = 12
const EmojiReactions = { const EmojiReactions = {
name: 'EmojiReactions', name: 'EmojiReactions',
components: {
UserAvatar
},
props: ['status'], props: ['status'],
data: () => ({
showAll: false,
popperOptions: {
modifiers: {
preventOverflow: { padding: { top: 50 }, boundariesElement: 'viewport' }
}
}
}),
computed: { computed: {
tooManyReactions () {
return this.status.emoji_reactions.length > EMOJI_REACTION_COUNT_CUTOFF
},
emojiReactions () { emojiReactions () {
return this.status.emoji_reactions 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: { methods: {
toggleShowAll () {
this.showAll = !this.showAll
},
reactedWith (emoji) { reactedWith (emoji) {
const user = this.$store.state.users.currentUser return this.status.emoji_reactions.find(r => r.name === emoji).me
const reaction = this.status.emoji_reactions.find(r => r.emoji === emoji) },
return reaction.accounts && reaction.accounts.find(u => u.id === user.id) fetchEmojiReactionsByIfMissing () {
const hasNoAccounts = this.status.emoji_reactions.find(r => !r.accounts)
if (hasNoAccounts) {
this.$store.dispatch('fetchEmojiReactionsBy', this.status.id)
}
}, },
reactWith (emoji) { reactWith (emoji) {
this.$store.dispatch('reactWithEmoji', { id: this.status.id, emoji }) this.$store.dispatch('reactWithEmoji', { id: this.status.id, emoji })
@ -20,6 +58,8 @@ const EmojiReactions = {
this.$store.dispatch('unreactWithEmoji', { id: this.status.id, emoji }) this.$store.dispatch('unreactWithEmoji', { id: this.status.id, emoji })
}, },
emojiOnClick (emoji, event) { emojiOnClick (emoji, event) {
if (!this.loggedIn) return
if (this.reactedWith(emoji)) { if (this.reactedWith(emoji)) {
this.unreact(emoji) this.unreact(emoji)
} else { } else {

View file

@ -1,16 +1,58 @@
<template> <template>
<div class="emoji-reactions"> <div class="emoji-reactions">
<button <v-popover
v-for="(reaction) in emojiReactions" v-for="(reaction) in emojiReactions"
:key="reaction.emoji" :key="reaction.name"
class="emoji-reaction btn btn-default" :popper-options="popperOptions"
:class="{ 'toggled': reactedWith(reaction.emoji) }" trigger="hover"
@click="emojiOnClick(reaction.emoji, $event)" placement="top"
> >
<span class="reaction-emoji">{{ reaction.emoji }}</span>
<span>{{ reaction.count }}</span> <div
</button> slot="popover"
class="reacted-users"
>
<div v-if="accountsForEmoji[reaction.name].length">
<div
v-for="(account) in accountsForEmoji[reaction.name]"
:key="account.id"
class="reacted-user"
>
<UserAvatar
:user="account"
class="avatar-small"
:compact="true"
/>
<div class="reacted-user-names">
<span class="reacted-user-name" v-html="account.name_html" />
<span class="reacted-user-screen-name">{{ account.screen_name }}</span>
</div>
</div>
</div>
<div v-else>
<i class="icon-spin4 animate-spin" />
</div>
</div>
<button
class="emoji-reaction btn btn-default"
:class="{ 'picked-reaction': reactedWith(reaction.name), 'not-clickable': !loggedIn }"
@click="emojiOnClick(reaction.name, $event)"
@mouseenter="fetchEmojiReactionsByIfMissing()"
>
<span class="reaction-emoji">{{ reaction.name }}</span>
<span>{{ reaction.count }}</span>
</button>
</v-popover>
<a
v-if="tooManyReactions"
@click="toggleShowAll"
class="emoji-reaction-expand faint"
href='javascript:void(0)'
>
{{ showAll ? $t('general.show_less') : showMoreString }}
</a>
</div> </div>
</template> </template>
<script src="./emoji_reactions.js" ></script> <script src="./emoji_reactions.js" ></script>
@ -23,6 +65,31 @@
flex-wrap: wrap; flex-wrap: wrap;
} }
.reacted-users {
padding: 0.5em;
}
.reacted-user {
padding: 0.25em;
display: flex;
flex-direction: row;
.reacted-user-names {
display: flex;
flex-direction: column;
margin-left: 0.5em;
img {
width: 1em;
height: 1em;
}
}
.reacted-user-screen-name {
font-size: 9px;
}
}
.emoji-reaction { .emoji-reaction {
padding: 0 0.5em; padding: 0 0.5em;
margin-right: 0.5em; margin-right: 0.5em;
@ -38,6 +105,26 @@
&:focus { &:focus {
outline: none; outline: none;
} }
&.not-clickable {
cursor: default;
&:hover {
box-shadow: $fallback--buttonShadow;
box-shadow: var(--buttonShadow);
}
}
}
.emoji-reaction-expand {
padding: 0 0.5em;
margin-right: 0.5em;
margin-top: 0.5em;
display: flex;
align-items: center;
justify-content: center;
&:hover {
text-decoration: underline;
}
} }
</style> </style>

View file

@ -10,6 +10,7 @@ const tabModeDict = {
const Interactions = { const Interactions = {
data () { data () {
return { return {
allowFollowingMove: this.$store.state.users.currentUser.allow_following_move,
filterMode: tabModeDict['mentions'] filterMode: tabModeDict['mentions']
} }
}, },

View file

@ -22,6 +22,7 @@
:label="$t('interactions.follows')" :label="$t('interactions.follows')"
/> />
<span <span
v-if="!allowFollowingMove"
key="moves" key="moves"
:label="$t('interactions.moves')" :label="$t('interactions.moves')"
/> />

View file

@ -11,7 +11,10 @@ const MRFTransparencyPanel = {
rejectInstances: state => get(state, 'instance.federationPolicy.mrf_simple.reject', []), rejectInstances: state => get(state, 'instance.federationPolicy.mrf_simple.reject', []),
ftlRemovalInstances: state => get(state, 'instance.federationPolicy.mrf_simple.federated_timeline_removal', []), ftlRemovalInstances: state => get(state, 'instance.federationPolicy.mrf_simple.federated_timeline_removal', []),
mediaNsfwInstances: state => get(state, 'instance.federationPolicy.mrf_simple.media_nsfw', []), 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 () { hasInstanceSpecificPolicies () {
return this.quarantineInstances.length || return this.quarantineInstances.length ||
@ -20,6 +23,11 @@ const MRFTransparencyPanel = {
this.ftlRemovalInstances.length || this.ftlRemovalInstances.length ||
this.mediaNsfwInstances.length || this.mediaNsfwInstances.length ||
this.mediaRemovalInstances.length this.mediaRemovalInstances.length
},
hasKeywordPolicies () {
return this.keywordsFtlRemoval.length ||
this.keywordsReject.length ||
this.keywordsReplace.length
} }
} }
} }

View file

@ -109,6 +109,49 @@
/> />
</ul> </ul>
</div> </div>
<h2 v-if="hasKeywordPolicies">
{{ $t("about.mrf.keyword.keyword_policies") }}
</h2>
<div v-if="keywordsFtlRemoval.length">
<h4>{{ $t("about.mrf.keyword.ftl_removal") }}</h4>
<ul>
<li
v-for="keyword in keywordsFtlRemoval"
:key="keyword"
v-text="keyword"
/>
</ul>
</div>
<div v-if="keywordsReject.length">
<h4>{{ $t("about.mrf.keyword.reject") }}</h4>
<ul>
<li
v-for="keyword in keywordsReject"
:key="keyword"
v-text="keyword"
/>
</ul>
</div>
<div v-if="keywordsReplace.length">
<h4>{{ $t("about.mrf.keyword.replace") }}</h4>
<ul>
<li
v-for="keyword in keywordsReplace"
:key="keyword"
>
{{ keyword.pattern }}
{{ $t("about.mrf.keyword.is_replaced_by") }}
{{ keyword.replacement }}
</li>
</ul>
</div>
</div> </div>
</div> </div>
</div> </div>

View file

@ -78,6 +78,13 @@
<i class="fa icon-arrow-curved lit" /> <i class="fa icon-arrow-curved lit" />
<small>{{ $t('notifications.migrated_to') }}</small> <small>{{ $t('notifications.migrated_to') }}</small>
</span> </span>
<span v-if="notification.type === 'pleroma:emoji_reaction'">
<small>
<i18n path="notifications.reacted_with">
<span class="emoji-reaction-emoji">{{ notification.emoji }}</span>
</i18n>
</small>
</span>
</div> </div>
<div <div
v-if="notification.type === 'follow' || notification.type === 'move'" v-if="notification.type === 'follow' || notification.type === 'move'"

View file

@ -97,6 +97,10 @@
min-width: 0; min-width: 0;
} }
.emoji-reaction-emoji {
font-size: 16px;
}
.notification-details { .notification-details {
min-width: 0px; min-width: 0px;
word-wrap: break-word; word-wrap: break-word;

View file

@ -22,7 +22,12 @@ const ReactButton = {
this.showTooltip = false this.showTooltip = false
}, },
addReaction (event, emoji) { addReaction (event, emoji) {
this.$store.dispatch('reactWithEmoji', { id: this.status.id, emoji }) const existingReaction = this.status.emoji_reactions.find(r => 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() this.closeReactionSelect()
} }
}, },

View file

@ -54,6 +54,10 @@
.reaction-picker-filter { .reaction-picker-filter {
padding: 0.5em; padding: 0.5em;
display: flex;
input {
flex: 1;
}
} }
.reaction-picker-divider { .reaction-picker-divider {

View file

@ -92,6 +92,11 @@
{{ $t('settings.reply_link_preview') }} {{ $t('settings.reply_link_preview') }}
</Checkbox> </Checkbox>
</li> </li>
<li>
<Checkbox v-model="emojiReactionsOnTimeline">
{{ $t('settings.emoji_reactions_on_timeline') }}
</Checkbox>
</li>
</ul> </ul>
</div> </div>
@ -328,6 +333,11 @@
{{ $t('settings.notification_visibility_moves') }} {{ $t('settings.notification_visibility_moves') }}
</Checkbox> </Checkbox>
</li> </li>
<li>
<Checkbox v-model="notificationVisibility.emojiReactions">
{{ $t('settings.notification_visibility_emoji_reactions') }}
</Checkbox>
</li>
</ul> </ul>
</div> </div>
<div> <div>

View file

@ -256,6 +256,16 @@ const Status = {
file => !fileType.fileMatchesSomeType(this.galleryTypes, file) 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 () { maxThumbnails () {
return this.mergedConfig.maxThumbnails return this.mergedConfig.maxThumbnails
}, },

View file

@ -277,7 +277,21 @@
href="#" href="#"
class="cw-status-hider" class="cw-status-hider"
@click.prevent="toggleShowMore" @click.prevent="toggleShowMore"
>{{ $t("general.show_more") }}</a> >
{{ $t("general.show_more") }}
<span
v-if="hasImageAttachments"
class="icon-picture"
/>
<span
v-if="hasVideoAttachments"
class="icon-video"
/>
<span
v-if="status.card"
class="icon-link"
/>
</a>
<a <a
v-if="showingMore" v-if="showingMore"
href="#" href="#"
@ -355,6 +369,7 @@
</transition> </transition>
<EmojiReactions <EmojiReactions
v-if="(mergedConfig.emojiReactionsOnTimeline || isFocused) && (!noHeading && !isPreview)"
:status="status" :status="status"
/> />

View file

@ -55,6 +55,7 @@ const UserSettings = {
showRole: this.$store.state.users.currentUser.show_role, showRole: this.$store.state.users.currentUser.show_role,
role: this.$store.state.users.currentUser.role, role: this.$store.state.users.currentUser.role,
discoverable: this.$store.state.users.currentUser.discoverable, discoverable: this.$store.state.users.currentUser.discoverable,
allowFollowingMove: this.$store.state.users.currentUser.allow_following_move,
pickAvatarBtnVisible: true, pickAvatarBtnVisible: true,
bannerUploading: false, bannerUploading: false,
backgroundUploading: false, backgroundUploading: false,
@ -162,6 +163,7 @@ const UserSettings = {
hide_follows: this.hideFollows, hide_follows: this.hideFollows,
hide_followers: this.hideFollowers, hide_followers: this.hideFollowers,
discoverable: this.discoverable, discoverable: this.discoverable,
allow_following_move: this.allowFollowingMove,
hide_follows_count: this.hideFollowsCount, hide_follows_count: this.hideFollowsCount,
hide_followers_count: this.hideFollowersCount, hide_followers_count: this.hideFollowersCount,
show_role: this.showRole show_role: this.showRole

View file

@ -90,9 +90,7 @@
</Checkbox> </Checkbox>
</p> </p>
<p> <p>
<Checkbox <Checkbox v-model="hideFollowers">
v-model="hideFollowers"
>
{{ $t('settings.hide_followers_description') }} {{ $t('settings.hide_followers_description') }}
</Checkbox> </Checkbox>
</p> </p>
@ -104,6 +102,11 @@
{{ $t('settings.hide_followers_count_description') }} {{ $t('settings.hide_followers_count_description') }}
</Checkbox> </Checkbox>
</p> </p>
<p>
<Checkbox v-model="allowFollowingMove">
{{ $t('settings.allow_following_move') }}
</Checkbox>
</p>
<p v-if="role === 'admin' || role === 'moderator'"> <p v-if="role === 'admin' || role === 'moderator'">
<Checkbox v-model="showRole"> <Checkbox v-model="showRole">
<template v-if="role === 'admin'"> <template v-if="role === 'admin'">

View file

@ -16,7 +16,16 @@
"mrf_policy_simple_media_removal": "Media Removal", "mrf_policy_simple_media_removal": "Media Removal",
"mrf_policy_simple_media_removal_desc": "This instance removes media from posts on the following instances:", "mrf_policy_simple_media_removal_desc": "This instance removes media from posts on the following instances:",
"mrf_policy_simple_media_nsfw": "Media Force-set As Sensitive", "mrf_policy_simple_media_nsfw": "Media Force-set As Sensitive",
"mrf_policy_simple_media_nsfw_desc": "This instance forces media to be set sensitive in posts on the following instances:" "mrf_policy_simple_media_nsfw_desc": "This instance forces media to be set sensitive in posts on the following instances:",
"mrf": {
"keyword": {
"keyword_policies": "Keyword Policies",
"ftl_removal": "Removal from \"The Whole Known Network\" Timeline",
"reject": "Reject",
"replace": "Replace",
"is_replaced_by": "→"
}
}
}, },
"chat": { "chat": {
"title": "Chat" "title": "Chat"
@ -118,7 +127,8 @@
"read": "Read!", "read": "Read!",
"repeated_you": "repeated your status", "repeated_you": "repeated your status",
"no_more_notifications": "No more notifications", "no_more_notifications": "No more notifications",
"migrated_to": "migrated to" "migrated_to": "migrated to",
"reacted_with": "reacted with {0}"
}, },
"polls": { "polls": {
"add_poll": "Add Poll", "add_poll": "Add Poll",
@ -233,6 +243,7 @@
"desc": "To enable two-factor authentication, enter the code from your two-factor app:" "desc": "To enable two-factor authentication, enter the code from your two-factor app:"
} }
}, },
"allow_following_move": "Allow auto-follow when following account moves",
"attachmentRadius": "Attachments", "attachmentRadius": "Attachments",
"attachments": "Attachments", "attachments": "Attachments",
"autoload": "Enable automatic loading when scrolled to the bottom", "autoload": "Enable automatic loading when scrolled to the bottom",
@ -274,6 +285,7 @@
"domain_mutes": "Domains", "domain_mutes": "Domains",
"avatar_size_instruction": "The recommended minimum size for avatar images is 150x150 pixels.", "avatar_size_instruction": "The recommended minimum size for avatar images is 150x150 pixels.",
"pad_emoji": "Pad emoji with spaces when adding from picker", "pad_emoji": "Pad emoji with spaces when adding from picker",
"emoji_reactions_on_timeline": "Show emoji reactions on timeline",
"export_theme": "Save preset", "export_theme": "Save preset",
"filtering": "Filtering", "filtering": "Filtering",
"filtering_explanation": "All statuses containing these words will be muted, one per line", "filtering_explanation": "All statuses containing these words will be muted, one per line",
@ -323,6 +335,7 @@
"notification_visibility_mentions": "Mentions", "notification_visibility_mentions": "Mentions",
"notification_visibility_repeats": "Repeats", "notification_visibility_repeats": "Repeats",
"notification_visibility_moves": "User Migrates", "notification_visibility_moves": "User Migrates",
"notification_visibility_emoji_reactions": "Reactions",
"no_rich_text_description": "Strip rich text formatting from all posts", "no_rich_text_description": "Strip rich text formatting from all posts",
"no_blocks": "No blocks", "no_blocks": "No blocks",
"no_mutes": "No mutes", "no_mutes": "No mutes",

View file

@ -53,7 +53,8 @@
"notifications": "Ilmoitukset", "notifications": "Ilmoitukset",
"read": "Lue!", "read": "Lue!",
"repeated_you": "toisti viestisi", "repeated_you": "toisti viestisi",
"no_more_notifications": "Ei enempää ilmoituksia" "no_more_notifications": "Ei enempää ilmoituksia",
"reacted_with": "lisäsi reaktion {0}"
}, },
"polls": { "polls": {
"add_poll": "Lisää äänestys", "add_poll": "Lisää äänestys",
@ -140,6 +141,7 @@
"delete_account_description": "Poista tilisi ja viestisi pysyvästi.", "delete_account_description": "Poista tilisi ja viestisi pysyvästi.",
"delete_account_error": "Virhe poistaessa tiliäsi. Jos virhe jatkuu, ota yhteyttä palvelimesi ylläpitoon.", "delete_account_error": "Virhe poistaessa tiliäsi. Jos virhe jatkuu, ota yhteyttä palvelimesi ylläpitoon.",
"delete_account_instructions": "Syötä salasanasi vahvistaaksesi tilin poiston.", "delete_account_instructions": "Syötä salasanasi vahvistaaksesi tilin poiston.",
"emoji_reactions_on_timeline": "Näytä emojireaktiot aikajanalla",
"export_theme": "Tallenna teema", "export_theme": "Tallenna teema",
"filtering": "Suodatus", "filtering": "Suodatus",
"filtering_explanation": "Kaikki viestit, jotka sisältävät näitä sanoja, suodatetaan. Yksi sana per rivi.", "filtering_explanation": "Kaikki viestit, jotka sisältävät näitä sanoja, suodatetaan. Yksi sana per rivi.",
@ -183,6 +185,7 @@
"notification_visibility_likes": "Tykkäykset", "notification_visibility_likes": "Tykkäykset",
"notification_visibility_mentions": "Maininnat", "notification_visibility_mentions": "Maininnat",
"notification_visibility_repeats": "Toistot", "notification_visibility_repeats": "Toistot",
"notification_visibility_emoji_reactions": "Reaktiot",
"no_rich_text_description": "Älä näytä tekstin muotoilua.", "no_rich_text_description": "Älä näytä tekstin muotoilua.",
"hide_network_description": "Älä näytä seurauksiani tai seuraajiani", "hide_network_description": "Älä näytä seurauksiani tai seuraajiani",
"nsfw_clickthrough": "Piilota NSFW liitteet klikkauksen taakse", "nsfw_clickthrough": "Piilota NSFW liitteet klikkauksen taakse",

View file

@ -23,6 +23,7 @@ export const defaultState = {
autoLoad: true, autoLoad: true,
streaming: false, streaming: false,
hoverPreview: true, hoverPreview: true,
emojiReactionsOnTimeline: true,
autohideFloatingPostButton: false, autohideFloatingPostButton: false,
pauseOnUnfocused: true, pauseOnUnfocused: true,
stopGifs: false, stopGifs: false,
@ -32,7 +33,8 @@ export const defaultState = {
mentions: true, mentions: true,
likes: true, likes: true,
repeats: true, repeats: true,
moves: true moves: true,
emojiReactions: false
}, },
webPushNotifications: false, webPushNotifications: false,
muteWords: [], muteWords: [],

View file

@ -81,7 +81,8 @@ const visibleNotificationTypes = (rootState) => {
rootState.config.notificationVisibility.mentions && 'mention', rootState.config.notificationVisibility.mentions && 'mention',
rootState.config.notificationVisibility.repeats && 'repeat', rootState.config.notificationVisibility.repeats && 'repeat',
rootState.config.notificationVisibility.follows && 'follow', rootState.config.notificationVisibility.follows && 'follow',
rootState.config.notificationVisibility.moves && 'move' rootState.config.notificationVisibility.moves && 'move',
rootState.config.notificationVisibility.emojiReactions && 'pleroma:emoji_reactions'
].filter(_ => _) ].filter(_ => _)
} }
@ -325,6 +326,10 @@ const addNewNotifications = (state, { dispatch, notifications, older, visibleNot
notification.status = notification.status && addStatusToGlobalStorage(state, notification.status).item notification.status = notification.status && addStatusToGlobalStorage(state, notification.status).item
} }
if (notification.type === 'pleroma:emoji_reaction') {
dispatch('fetchEmojiReactionsBy', notification.status.id)
}
// Only add a new notification if we don't have one for the same action // Only add a new notification if we don't have one for the same action
if (!state.notifications.idStore.hasOwnProperty(notification.id)) { if (!state.notifications.idStore.hasOwnProperty(notification.id)) {
state.notifications.maxId = notification.id > state.notifications.maxId state.notifications.maxId = notification.id > state.notifications.maxId
@ -358,7 +363,9 @@ const addNewNotifications = (state, { dispatch, notifications, older, visibleNot
break break
} }
if (i18nString) { 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) notifObj.body = rootGetters.i18n.t('notifications.' + i18nString)
} else { } else {
notifObj.body = notification.status.text notifObj.body = notification.status.text
@ -371,10 +378,10 @@ const addNewNotifications = (state, { dispatch, notifications, older, visibleNot
} }
if (!notification.seen && !state.notifications.desktopNotificationSilence && visibleNotificationTypes.includes(notification.type)) { if (!notification.seen && !state.notifications.desktopNotificationSilence && visibleNotificationTypes.includes(notification.type)) {
let notification = new window.Notification(title, notifObj) let desktopNotification = new window.Notification(title, notifObj)
// Chrome is known for not closing notifications automatically // Chrome is known for not closing notifications automatically
// according to MDN, anyway. // according to MDN, anyway.
setTimeout(notification.close.bind(notification), 5000) setTimeout(desktopNotification.close.bind(desktopNotification), 5000)
} }
} }
} else if (notification.seen) { } else if (notification.seen) {
@ -537,12 +544,13 @@ export const mutations = {
}, },
addOwnReaction (state, { id, emoji, currentUser }) { addOwnReaction (state, { id, emoji, currentUser }) {
const status = state.allStatusesObject[id] const status = state.allStatusesObject[id]
const reactionIndex = findIndex(status.emoji_reactions, { emoji }) const reactionIndex = findIndex(status.emoji_reactions, { name: emoji })
const reaction = status.emoji_reactions[reactionIndex] || { emoji, count: 0, accounts: [] } const reaction = status.emoji_reactions[reactionIndex] || { name: emoji, count: 0, accounts: [] }
const newReaction = { const newReaction = {
...reaction, ...reaction,
count: reaction.count + 1, count: reaction.count + 1,
me: true,
accounts: [ accounts: [
...reaction.accounts, ...reaction.accounts,
currentUser currentUser
@ -558,21 +566,23 @@ export const mutations = {
}, },
removeOwnReaction (state, { id, emoji, currentUser }) { removeOwnReaction (state, { id, emoji, currentUser }) {
const status = state.allStatusesObject[id] const status = state.allStatusesObject[id]
const reactionIndex = findIndex(status.emoji_reactions, { emoji }) const reactionIndex = findIndex(status.emoji_reactions, { name: emoji })
if (reactionIndex < 0) return if (reactionIndex < 0) return
const reaction = status.emoji_reactions[reactionIndex] const reaction = status.emoji_reactions[reactionIndex]
const accounts = reaction.accounts || []
const newReaction = { const newReaction = {
...reaction, ...reaction,
count: reaction.count - 1, count: reaction.count - 1,
accounts: reaction.accounts.filter(acc => acc.id === currentUser.id) me: false,
accounts: accounts.filter(acc => acc.id !== currentUser.id)
} }
if (newReaction.count > 0) { if (newReaction.count > 0) {
set(status.emoji_reactions, reactionIndex, newReaction) set(status.emoji_reactions, reactionIndex, newReaction)
} else { } else {
set(status, 'emoji_reactions', status.emoji_reactions.filter(r => r.emoji !== emoji)) set(status, 'emoji_reactions', status.emoji_reactions.filter(r => r.name !== emoji))
} }
}, },
updateStatusWithPoll (state, { id, poll }) { updateStatusWithPoll (state, { id, poll }) {
@ -681,18 +691,22 @@ const statuses = {
}, },
reactWithEmoji ({ rootState, dispatch, commit }, { id, emoji }) { reactWithEmoji ({ rootState, dispatch, commit }, { id, emoji }) {
const currentUser = rootState.users.currentUser const currentUser = rootState.users.currentUser
if (!currentUser) return
commit('addOwnReaction', { id, emoji, currentUser }) commit('addOwnReaction', { id, emoji, currentUser })
rootState.api.backendInteractor.reactWithEmoji({ id, emoji }).then( rootState.api.backendInteractor.reactWithEmoji({ id, emoji }).then(
status => { ok => {
dispatch('fetchEmojiReactionsBy', id) dispatch('fetchEmojiReactionsBy', id)
} }
) )
}, },
unreactWithEmoji ({ rootState, dispatch, commit }, { id, emoji }) { unreactWithEmoji ({ rootState, dispatch, commit }, { id, emoji }) {
const currentUser = rootState.users.currentUser const currentUser = rootState.users.currentUser
if (!currentUser) return
commit('removeOwnReaction', { id, emoji, currentUser }) commit('removeOwnReaction', { id, emoji, currentUser })
rootState.api.backendInteractor.unreactWithEmoji({ id, emoji }).then( rootState.api.backendInteractor.unreactWithEmoji({ id, emoji }).then(
status => { ok => {
dispatch('fetchEmojiReactionsBy', id) dispatch('fetchEmojiReactionsBy', id)
} }
) )

View file

@ -74,9 +74,9 @@ const MASTODON_SEARCH_2 = `/api/v2/search`
const MASTODON_USER_SEARCH_URL = '/api/v1/accounts/search' const MASTODON_USER_SEARCH_URL = '/api/v1/accounts/search'
const MASTODON_DOMAIN_BLOCKS_URL = '/api/v1/domain_blocks' const MASTODON_DOMAIN_BLOCKS_URL = '/api/v1/domain_blocks'
const MASTODON_STREAMING = '/api/v1/streaming' const MASTODON_STREAMING = '/api/v1/streaming'
const PLEROMA_EMOJI_REACTIONS_URL = id => `/api/v1/pleroma/statuses/${id}/emoji_reactions_by` const PLEROMA_EMOJI_REACTIONS_URL = id => `/api/v1/pleroma/statuses/${id}/reactions`
const PLEROMA_EMOJI_REACT_URL = id => `/api/v1/pleroma/statuses/${id}/react_with_emoji` const PLEROMA_EMOJI_REACT_URL = (id, emoji) => `/api/v1/pleroma/statuses/${id}/reactions/${emoji}`
const PLEROMA_EMOJI_UNREACT_URL = id => `/api/v1/pleroma/statuses/${id}/unreact_with_emoji` const PLEROMA_EMOJI_UNREACT_URL = (id, emoji) => `/api/v1/pleroma/statuses/${id}/reactions/${emoji}`
const oldfetch = window.fetch const oldfetch = window.fetch
@ -495,7 +495,8 @@ const fetchTimeline = ({
until = false, until = false,
userId = false, userId = false,
tag = false, tag = false,
withMuted = false withMuted = false,
withMove = false
}) => { }) => {
const timelineUrls = { const timelineUrls = {
public: MASTODON_PUBLIC_TIMELINE, public: MASTODON_PUBLIC_TIMELINE,
@ -535,6 +536,9 @@ const fetchTimeline = ({
if (timeline === 'public' || timeline === 'publicAndExternal') { if (timeline === 'public' || timeline === 'publicAndExternal') {
params.push(['only_media', false]) params.push(['only_media', false])
} }
if (timeline === 'notifications') {
params.push(['with_move', withMove])
}
params.push(['count', 20]) params.push(['count', 20])
params.push(['with_muted', withMuted]) params.push(['with_muted', withMuted])
@ -884,25 +888,27 @@ const fetchRebloggedByUsers = ({ id }) => {
return promisedRequest({ url: MASTODON_STATUS_REBLOGGEDBY_URL(id) }).then((users) => users.map(parseUser)) return promisedRequest({ url: MASTODON_STATUS_REBLOGGEDBY_URL(id) }).then((users) => users.map(parseUser))
} }
const fetchEmojiReactions = ({ id }) => { const fetchEmojiReactions = ({ id, credentials }) => {
return promisedRequest({ url: PLEROMA_EMOJI_REACTIONS_URL(id) }) return promisedRequest({ url: PLEROMA_EMOJI_REACTIONS_URL(id), credentials })
.then((reactions) => reactions.map(r => {
r.accounts = r.accounts.map(parseUser)
return r
}))
} }
const reactWithEmoji = ({ id, emoji, credentials }) => { const reactWithEmoji = ({ id, emoji, credentials }) => {
return promisedRequest({ return promisedRequest({
url: PLEROMA_EMOJI_REACT_URL(id), url: PLEROMA_EMOJI_REACT_URL(id, emoji),
method: 'POST', method: 'PUT',
credentials, credentials
payload: { emoji }
}).then(parseStatus) }).then(parseStatus)
} }
const unreactWithEmoji = ({ id, emoji, credentials }) => { const unreactWithEmoji = ({ id, emoji, credentials }) => {
return promisedRequest({ return promisedRequest({
url: PLEROMA_EMOJI_UNREACT_URL(id), url: PLEROMA_EMOJI_UNREACT_URL(id, emoji),
method: 'POST', method: 'DELETE',
credentials, credentials
payload: { emoji }
}).then(parseStatus) }).then(parseStatus)
} }

View file

@ -83,6 +83,8 @@ export const parseUser = (data) => {
output.subscribed = relationship.subscribing output.subscribed = relationship.subscribing
} }
output.allow_following_move = data.pleroma.allow_following_move
output.hide_follows = data.pleroma.hide_follows output.hide_follows = data.pleroma.hide_follows
output.hide_followers = data.pleroma.hide_followers output.hide_followers = data.pleroma.hide_followers
output.hide_follows_count = data.pleroma.hide_follows_count output.hide_follows_count = data.pleroma.hide_follows_count
@ -352,6 +354,7 @@ export const parseNotification = (data) => {
? null ? null
: parseUser(data.target) : parseUser(data.target)
output.from_profile = parseUser(data.account) output.from_profile = parseUser(data.account)
output.emoji = data.emoji
} else { } else {
const parsedNotice = parseStatus(data.notice) const parsedNotice = parseStatus(data.notice)
output.type = data.ntype output.type = data.ntype

View file

@ -7,7 +7,8 @@ export const visibleTypes = store => ([
store.state.config.notificationVisibility.mentions && 'mention', store.state.config.notificationVisibility.mentions && 'mention',
store.state.config.notificationVisibility.repeats && 'repeat', store.state.config.notificationVisibility.repeats && 'repeat',
store.state.config.notificationVisibility.follows && 'follow', store.state.config.notificationVisibility.follows && 'follow',
store.state.config.notificationVisibility.moves && 'move' store.state.config.notificationVisibility.moves && 'move',
store.state.config.notificationVisibility.emojiReactions && 'pleroma:emoji_reaction'
].filter(_ => _)) ].filter(_ => _))
const sortById = (a, b) => { const sortById = (a, b) => {

View file

@ -11,9 +11,12 @@ const fetchAndUpdate = ({ store, credentials, older = false }) => {
const rootState = store.rootState || store.state const rootState = store.rootState || store.state
const timelineData = rootState.statuses.notifications const timelineData = rootState.statuses.notifications
const hideMutedPosts = getters.mergedConfig.hideMutedPosts const hideMutedPosts = getters.mergedConfig.hideMutedPosts
const allowFollowingMove = rootState.users.currentUser.allow_following_move
args['withMuted'] = !hideMutedPosts args['withMuted'] = !hideMutedPosts
args['withMove'] = !allowFollowingMove
args['timeline'] = 'notifications' args['timeline'] = 'notifications'
if (older) { if (older) {
if (timelineData.minId !== Number.POSITIVE_INFINITY) { if (timelineData.minId !== Number.POSITIVE_INFINITY) {

View file

@ -339,6 +339,12 @@
"css": "arrow-curved", "css": "arrow-curved",
"code": 59426, "code": 59426,
"src": "iconic" "src": "iconic"
},
{
"uid": "0ddd3e8201ccc7d41f7b7c9d27eca6c1",
"css": "link",
"code": 59427,
"src": "fontawesome"
} }
] ]
} }

View file

@ -245,11 +245,12 @@ describe('Statuses module', () => {
it('increments count in existing reaction', () => { it('increments count in existing reaction', () => {
const state = defaultState() const state = defaultState()
const status = makeMockStatus({ id: '1' }) const status = makeMockStatus({ id: '1' })
status.emoji_reactions = [ { emoji: '😂', count: 1, accounts: [] } ] status.emoji_reactions = [ { name: '😂', count: 1, accounts: [] } ]
mutations.addNewStatuses(state, { statuses: [status], showImmediately: true, timeline: 'public' }) mutations.addNewStatuses(state, { statuses: [status], showImmediately: true, timeline: 'public' })
mutations.addOwnReaction(state, { id: '1', emoji: '😂', currentUser: { id: 'me' } }) mutations.addOwnReaction(state, { id: '1', emoji: '😂', currentUser: { id: 'me' } })
expect(state.allStatusesObject['1'].emoji_reactions[0].count).to.eql(2) expect(state.allStatusesObject['1'].emoji_reactions[0].count).to.eql(2)
expect(state.allStatusesObject['1'].emoji_reactions[0].me).to.eql(true)
expect(state.allStatusesObject['1'].emoji_reactions[0].accounts[0].id).to.eql('me') expect(state.allStatusesObject['1'].emoji_reactions[0].accounts[0].id).to.eql('me')
}) })
@ -261,27 +262,29 @@ describe('Statuses module', () => {
mutations.addNewStatuses(state, { statuses: [status], showImmediately: true, timeline: 'public' }) mutations.addNewStatuses(state, { statuses: [status], showImmediately: true, timeline: 'public' })
mutations.addOwnReaction(state, { id: '1', emoji: '😂', currentUser: { id: 'me' } }) mutations.addOwnReaction(state, { id: '1', emoji: '😂', currentUser: { id: 'me' } })
expect(state.allStatusesObject['1'].emoji_reactions[0].count).to.eql(1) expect(state.allStatusesObject['1'].emoji_reactions[0].count).to.eql(1)
expect(state.allStatusesObject['1'].emoji_reactions[0].me).to.eql(true)
expect(state.allStatusesObject['1'].emoji_reactions[0].accounts[0].id).to.eql('me') expect(state.allStatusesObject['1'].emoji_reactions[0].accounts[0].id).to.eql('me')
}) })
it('decreases count in existing reaction', () => { it('decreases count in existing reaction', () => {
const state = defaultState() const state = defaultState()
const status = makeMockStatus({ id: '1' }) const status = makeMockStatus({ id: '1' })
status.emoji_reactions = [ { emoji: '😂', count: 2, accounts: [{ id: 'me' }] } ] status.emoji_reactions = [ { name: '😂', count: 2, accounts: [{ id: 'me' }] } ]
mutations.addNewStatuses(state, { statuses: [status], showImmediately: true, timeline: 'public' }) mutations.addNewStatuses(state, { statuses: [status], showImmediately: true, timeline: 'public' })
mutations.removeOwnReaction(state, { id: '1', emoji: '😂', currentUser: {} }) mutations.removeOwnReaction(state, { id: '1', emoji: '😂', currentUser: { id: 'me' } })
expect(state.allStatusesObject['1'].emoji_reactions[0].count).to.eql(1) expect(state.allStatusesObject['1'].emoji_reactions[0].count).to.eql(1)
expect(state.allStatusesObject['1'].emoji_reactions[0].me).to.eql(false)
expect(state.allStatusesObject['1'].emoji_reactions[0].accounts).to.eql([]) expect(state.allStatusesObject['1'].emoji_reactions[0].accounts).to.eql([])
}) })
it('removes a reaction', () => { it('removes a reaction', () => {
const state = defaultState() const state = defaultState()
const status = makeMockStatus({ id: '1' }) const status = makeMockStatus({ id: '1' })
status.emoji_reactions = [{ emoji: '😂', count: 1, accounts: [{ id: 'me' }] }] status.emoji_reactions = [{ name: '😂', count: 1, accounts: [{ id: 'me' }] }]
mutations.addNewStatuses(state, { statuses: [status], showImmediately: true, timeline: 'public' }) mutations.addNewStatuses(state, { statuses: [status], showImmediately: true, timeline: 'public' })
mutations.removeOwnReaction(state, { id: '1', emoji: '😂', currentUser: {} }) mutations.removeOwnReaction(state, { id: '1', emoji: '😂', currentUser: { id: 'me' } })
expect(state.allStatusesObject['1'].emoji_reactions.length).to.eql(0) expect(state.allStatusesObject['1'].emoji_reactions.length).to.eql(0)
}) })
}) })