Merge remote-tracking branch 'origin/develop' into settings-modal

* origin/develop: (22 commits)
  changelog
  alignment fixes
  Update CHANGELOG.md
  StillImage: Make it work properly in both firefox and chrome.
  ReactButton: Change the combined emoji (heart) to a simple one.
  Linting fixes.
  Settings: Keep a local version of the mutedWordsString
  MediaModal: Close on browser navigation events.
  StatusContent: Try to get hashtag from dataset first.
  Docs: Change wrong documentation.
  EntityNormalizerSpec: More fixes.
  EntityNormalizerSpec: Test new behavior.
  EntityNormalizer: Add colons to emoji alt text.
  fixed case in class name
  The sidebarRight option wasn't being read
  Use consistent naming for Pleroma-FE in README
  Remove mention of GNU Social
  lint
  multiple fixes
  fix non-mention notifs
  ...
This commit is contained in:
Henry Jameson 2020-06-07 00:10:21 +03:00
commit 9e3e6b0c69
21 changed files with 195 additions and 64 deletions

View file

@ -6,6 +6,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
### Changed ### Changed
- Removed the use of with_move parameters when fetching notifications - Removed the use of with_move parameters when fetching notifications
### Fixed
- Multiple issues with muted statuses/notifications
## [Unreleased patch] ## [Unreleased patch]
### Add ### Add
- Added private notifications option for push notifications - Added private notifications option for push notifications
@ -13,9 +16,15 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
### Changed ### Changed
- Registration page no longer requires email if the server is configured not to require it - 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
### Fixed ### Fixed
- Status ellipsis menu closes properly when selecting certain options - 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
## [2.0.3] - 2020-05-02 ## [2.0.3] - 2020-05-02
### Fixed ### Fixed

View file

@ -1,6 +1,6 @@
# pleroma_fe # Pleroma-FE
> A single column frontend for both Pleroma and GS servers. > A single column frontend designed for Pleroma.
![screenshot](https://i.imgur.com/DJVqSJ0.png) ![screenshot](https://i.imgur.com/DJVqSJ0.png)
@ -11,7 +11,6 @@ To translate Pleroma-FE, add your language to [src/i18n/messages.js](https://git
# FOR ADMINS # FOR ADMINS
You don't need to build Pleroma-FE yourself. Those using the Pleroma backend will be able to use it out of the box. You don't need to build Pleroma-FE yourself. Those using the Pleroma backend will be able to use it out of the box.
For the GNU social backend, check out https://git.pleroma.social/pleroma/pleroma-fe/wikis/dual-boot-with-qvitter to see how to run Pleroma-FE and Qvitter at the same time.
## Build Setup ## Build Setup

View file

@ -33,7 +33,7 @@ will become
Note that you can only use emoji defined on your instance, you cannot "copy" someone else's emoji, and will have to ask your administrator to copy emoji from other instance to yours. Note that you can only use emoji defined on your instance, you cannot "copy" someone else's emoji, and will have to ask your administrator to copy emoji from other instance to yours.
Lastly, there's two convenience options for emoji: an emoji picker (smiley face to the right of "submit" button) and autocomplete suggestions - when you start typing :shortcode: it will automatically try to suggest you emoj and complete the shortcode for you if you select one. **Note** that if emoji doesn't show up in suggestions nor in emoji picker it means there's no such emoji on your instance, if shortcode doesn't match any defined emoji it will appear as text. Lastly, there's two convenience options for emoji: an emoji picker (smiley face to the right of "submit" button) and autocomplete suggestions - when you start typing :shortcode: it will automatically try to suggest you emoj and complete the shortcode for you if you select one. **Note** that if emoji doesn't show up in suggestions nor in emoji picker it means there's no such emoji on your instance, if shortcode doesn't match any defined emoji it will appear as text.
* **Attachments** are fairly simple - you can attach any file to a post as long as the file is within maximum size limits. If you're uploading explicit material you can mark all of your attachments as sensitive (or add `#nsfw` tag) - it will hide the images and videos behind a warning so that it won't be displayed instantly. * **Attachments** are fairly simple - you can attach any file to a post as long as the file is within maximum size limits. If you're uploading explicit material you can mark all of your attachments as sensitive (or add `#nsfw` tag) - it will hide the images and videos behind a warning so that it won't be displayed instantly.
* **Subject line** also known as **CW** (Content Warning) could be used as a header to the post and/or to warn others about contents of the post having something that might upset somebody or something among those lines. Several applications allow to hide post content leaving only subject line visible. As a side-effect using subject line will also mark your images as sensitive (see above). * **Subject line** also known as **CW** (Content Warning) could be used as a header to the post and/or to warn others about contents of the post having something that might upset somebody or something among those lines. Several applications allow to hide post content leaving only subject line visible. Using a subject line will not mark your images as sensitive, you will have to do that explicitly (see above).
* **Visiblity scope** controls who will be able to see your posts. There are four scopes available: * **Visiblity scope** controls who will be able to see your posts. There are four scopes available:
1. `Public`: This is the default, and some fediverse software like GNU Social only supports this. This means that your post is accessible by anyone and will be shown in the public timelines. 1. `Public`: This is the default, and some fediverse software like GNU Social only supports this. This means that your post is accessible by anyone and will be shown in the public timelines.

View file

@ -110,6 +110,7 @@ const setSettings = async ({ apiConfig, staticConfig, store }) => {
copyInstanceOption('alwaysShowSubjectInput') copyInstanceOption('alwaysShowSubjectInput')
copyInstanceOption('showFeaturesPanel') copyInstanceOption('showFeaturesPanel')
copyInstanceOption('hideSitename') copyInstanceOption('hideSitename')
copyInstanceOption('sidebarRight')
return store.dispatch('setTheme', config['theme']) return store.dispatch('setTheme', config['theme'])
} }

View file

@ -84,10 +84,12 @@ const MediaModal = {
} }
}, },
mounted () { mounted () {
window.addEventListener('popstate', this.hide)
document.addEventListener('keyup', this.handleKeyupEvent) document.addEventListener('keyup', this.handleKeyupEvent)
document.addEventListener('keydown', this.handleKeydownEvent) document.addEventListener('keydown', this.handleKeydownEvent)
}, },
destroyed () { destroyed () {
window.removeEventListener('popstate', this.hide)
document.removeEventListener('keyup', this.handleKeyupEvent) document.removeEventListener('keyup', this.handleKeyupEvent)
document.removeEventListener('keydown', this.handleKeydownEvent) document.removeEventListener('keydown', this.handleKeydownEvent)
} }

View file

@ -1,3 +1,4 @@
import StatusContent from '../status_content/status_content.vue'
import Status from '../status/status.vue' import Status from '../status/status.vue'
import UserAvatar from '../user_avatar/user_avatar.vue' import UserAvatar from '../user_avatar/user_avatar.vue'
import UserCard from '../user_card/user_card.vue' import UserCard from '../user_card/user_card.vue'
@ -16,10 +17,11 @@ const Notification = {
}, },
props: [ 'notification' ], props: [ 'notification' ],
components: { components: {
Status, StatusContent,
UserAvatar, UserAvatar,
UserCard, UserCard,
Timeago Timeago,
Status
}, },
methods: { methods: {
toggleUserExpanded () { toggleUserExpanded () {

View file

@ -157,11 +157,9 @@
</router-link> </router-link>
</div> </div>
<template v-else> <template v-else>
<status <status-content
class="faint" class="faint"
:compact="true" :status="notification.action"
:statusoid="notification.action"
:no-heading="true"
/> />
</template> </template>
</div> </div>

View file

@ -36,6 +36,8 @@
border-bottom: 1px solid; border-bottom: 1px solid;
border-color: $fallback--border; border-color: $fallback--border;
border-color: var(--border, $fallback--border); border-color: var(--border, $fallback--border);
word-wrap: break-word;
word-break: break-word;
&:hover .animated.avatar { &:hover .animated.avatar {
canvas { canvas {
@ -46,10 +48,6 @@
} }
} }
.muted {
padding: .25em .6em;
}
.non-mention { .non-mention {
display: flex; display: flex;
flex: 1; flex: 1;

View file

@ -24,7 +24,7 @@ const ReactButton = {
}, },
computed: { computed: {
commonEmojis () { commonEmojis () {
return ['❤️', '😠', '👀', '😂', '🔥'] return ['👍', '😠', '👀', '😂', '🔥']
}, },
emojis () { emojis () {
if (this.filterWord !== '') { if (this.filterWord !== '') {

View file

@ -1,13 +1,31 @@
import { filter, trim } from 'lodash'
import Checkbox from 'src/components/checkbox/checkbox.vue' import Checkbox from 'src/components/checkbox/checkbox.vue'
import SharedComputedObject from './helpers/shared_computed_object.js' import SharedComputedObject from './helpers/shared_computed_object.js'
const FilteringTab = { const FilteringTab = {
data () {
return {
muteWordsStringLocal: this.$store.getters.mergedConfig.muteWords.join('\n')
}
},
components: { components: {
Checkbox Checkbox
}, },
computed: { computed: {
...SharedComputedObject() ...SharedComputedObject(),
muteWordsString: {
get () {
return this.muteWordsStringLocal
},
set (value) {
this.muteWordsStringLocal = value
this.$store.dispatch('setOption', {
name: 'muteWords',
value: filter(value.split('\n'), (word) => trim(word).length > 0)
})
}
}
}, },
// Updating nested properties // Updating nested properties
watch: { watch: {

View file

@ -1,4 +1,3 @@
import { filter, trim } from 'lodash'
import { import {
instanceDefaultProperties, instanceDefaultProperties,
multiChoiceProperties, multiChoiceProperties,
@ -38,15 +37,6 @@ const SharedComputedObject = () => ({
}]) }])
.reduce((acc, [key, value]) => ({ ...acc, [key]: value }), {}), .reduce((acc, [key, value]) => ({ ...acc, [key]: value }), {}),
// Special cases (need to transform values or perform actions first) // Special cases (need to transform values or perform actions first)
muteWordsString: {
get () { return this.$store.getters.mergedConfig.muteWords.join('\n') },
set (value) {
this.$store.dispatch('setOption', {
name: 'muteWords',
value: filter(value.split('\n'), (word) => trim(word).length > 0)
})
}
},
useStreamingApi: { useStreamingApi: {
get () { return this.$store.getters.mergedConfig.useStreamingApi }, get () { return this.$store.getters.mergedConfig.useStreamingApi },
set (value) { set (value) {

View file

@ -12,7 +12,8 @@ import StatusPopover from '../status_popover/status_popover.vue'
import EmojiReactions from '../emoji_reactions/emoji_reactions.vue' import EmojiReactions from '../emoji_reactions/emoji_reactions.vue'
import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator' import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
import { highlightClass, highlightStyle } from '../../services/user_highlighter/user_highlighter.js' import { highlightClass, highlightStyle } from '../../services/user_highlighter/user_highlighter.js'
import { filter, unescape, uniqBy } from 'lodash' import { muteWordHits } from '../../services/status_parser/status_parser.js'
import { unescape, uniqBy } from 'lodash'
import { mapGetters, mapState } from 'vuex' import { mapGetters, mapState } from 'vuex'
const Status = { const Status = {
@ -44,6 +45,12 @@ const Status = {
muteWords () { muteWords () {
return this.mergedConfig.muteWords return this.mergedConfig.muteWords
}, },
showReasonMutedThread () {
return (
this.status.thread_muted ||
(this.status.reblog && this.status.reblog.thread_muted)
) && !this.inConversation
},
repeaterClass () { repeaterClass () {
const user = this.statusoid.user const user = this.statusoid.user
return highlightClass(user) return highlightClass(user)
@ -93,20 +100,42 @@ const Status = {
return !!this.currentUser return !!this.currentUser
}, },
muteWordHits () { muteWordHits () {
const statusText = this.status.text.toLowerCase() return muteWordHits(this.status, this.muteWords)
const statusSummary = this.status.summary.toLowerCase()
const hits = filter(this.muteWords, (muteWord) => {
return statusText.includes(muteWord.toLowerCase()) || statusSummary.includes(muteWord.toLowerCase())
})
return hits
}, },
muted () { muted () {
const relationship = this.$store.getters.relationship(this.status.user.id) const { status } = this
return !this.unmuted && ( const { reblog } = status
(!(this.inProfile && this.status.user.id === this.profileUserId) && relationship.muting) || const relationship = this.$store.getters.relationship(status.user.id)
(!this.inConversation && this.status.thread_muted) || const relationshipReblog = reblog && this.$store.getters.relationship(reblog.user.id)
this.muteWordHits.length > 0) const reasonsToMute = (
// Post is muted according to BE
status.muted ||
// Reprööt of a muted post according to BE
(reblog && reblog.muted) ||
// Muted user
relationship.muting ||
// Muted user of a reprööt
(relationshipReblog && relationshipReblog.muting) ||
// Thread is muted
status.thread_muted ||
// Wordfiltered
this.muteWordHits.length > 0
)
const excusesNotToMute = (
(
this.inProfile && (
// Don't mute user's posts on user timeline (except reblogs)
(!reblog && status.user.id === this.profileUserId) ||
// Same as above but also allow self-reblogs
(reblog && reblog.user.id === this.profileUserId)
)
) ||
// Don't mute statuses in muted conversation when said conversation is opened
(this.inConversation && status.thread_muted)
// No excuses if post has muted words
) && !this.muteWordHits.length > 0
return !this.unmuted && !excusesNotToMute && reasonsToMute
}, },
hideFilteredStatuses () { hideFilteredStatuses () {
return this.mergedConfig.hideFilteredStatuses return this.mergedConfig.hideFilteredStatuses

View file

@ -17,12 +17,33 @@
</div> </div>
<template v-if="muted && !isPreview"> <template v-if="muted && !isPreview">
<div class="media status container muted"> <div class="media status container muted">
<small> <small class="username">
<i
v-if="muted && retweet"
class="button-icon icon-retweet"
/>
<router-link :to="userProfileLink"> <router-link :to="userProfileLink">
{{ status.user.screen_name }} {{ status.user.screen_name }}
</router-link> </router-link>
</small> </small>
<small class="muteWords">{{ muteWordHits.join(', ') }}</small> <small
v-if="showReasonMutedThread"
class="mute-thread"
>
{{ $t('status.thread_muted') }}
</small>
<small
v-if="showReasonMutedThread && muteWordHits.length > 0"
class="mute-thread"
>
{{ $t('status.thread_muted_and_words') }}
</small>
<small
class="mute-words"
:title="muteWordHits.join(', ')"
>
{{ muteWordHits.join(', ') }}
</small>
<a <a
href="#" href="#"
class="unmute" class="unmute"
@ -637,19 +658,48 @@ $status-margin: 0.75em;
} }
.muted { .muted {
padding: 0.25em 0.5em; padding: .25em .6em;
button { height: 1.2em;
line-height: 1.2em;
text-overflow: ellipsis;
overflow: hidden;
display: flex;
flex-wrap: nowrap;
.username, .mute-thread, .mute-words {
word-wrap: normal;
word-break: normal;
white-space: nowrap;
}
.username, .mute-words {
text-overflow: ellipsis;
overflow: hidden;
}
.username {
flex: 0 1 auto;
margin-right: .2em;
}
.mute-thread {
flex: 0 0 auto;
}
.mute-words {
flex: 1 0 5em;
margin-left: .2em;
&::before {
content: ' '
}
}
.unmute {
flex: 0 0 auto;
margin-left: auto;
display: block;
margin-left: auto; margin-left: auto;
} }
.muteWords {
margin-left: 10px;
}
}
a.unmute {
display: block;
margin-left: auto;
} }
.reply-body { .reply-body {

View file

@ -176,8 +176,8 @@ const StatusContent = {
} }
} }
if (target.rel.match(/(?:^|\s)tag(?:$|\s)/) || target.className.match(/hashtag/)) { if (target.rel.match(/(?:^|\s)tag(?:$|\s)/) || target.className.match(/hashtag/)) {
// Extract tag name from link url // Extract tag name from dataset or link url
const tag = extractTagFromUrl(target.href) const tag = target.dataset.tag || extractTagFromUrl(target.href)
if (tag) { if (tag) {
const link = this.generateTagLink(tag) const link = this.generateTagLink(tag)
this.$router.push(link) this.$router.push(link)

View file

@ -23,12 +23,21 @@
<style lang="scss"> <style lang="scss">
@import '../../_variables.scss'; @import '../../_variables.scss';
.contain-fit {
.still-image {
img {
height: 100%;
}
}
}
.still-image { .still-image {
position: relative; position: relative;
line-height: 0; line-height: 0;
overflow: hidden; overflow: hidden;
width: 100%; width: 100%;
height: 100%; height: 100%;
display: flex;
&:hover canvas { &:hover canvas {
display: none; display: none;
@ -36,8 +45,8 @@
img { img {
width: 100%; width: 100%;
height: 100%;
object-fit: contain; object-fit: contain;
align-self: center;
} }
&.animated { &.animated {

View file

@ -627,7 +627,9 @@
"mute_conversation": "Mute conversation", "mute_conversation": "Mute conversation",
"unmute_conversation": "Unmute conversation", "unmute_conversation": "Unmute conversation",
"status_unavailable": "Status unavailable", "status_unavailable": "Status unavailable",
"copy_link": "Copy link to status" "copy_link": "Copy link to status",
"thread_muted": "Thread muted",
"thread_muted_and_words": ", has words:"
}, },
"user_card": { "user_card": {
"approve": "Approve", "approve": "Approve",

View file

@ -15,7 +15,7 @@ import {
import { set } from 'vue' import { set } from 'vue'
import { isStatusNotification } from '../services/notification_utils/notification_utils.js' import { isStatusNotification } from '../services/notification_utils/notification_utils.js'
import apiService from '../services/api/api.service.js' import apiService from '../services/api/api.service.js'
// import parse from '../services/status_parser/status_parser.js' import { muteWordHits } from '../services/status_parser/status_parser.js'
const emptyTl = (userId = 0) => ({ const emptyTl = (userId = 0) => ({
statuses: [], statuses: [],
@ -381,7 +381,18 @@ const addNewNotifications = (state, { dispatch, notifications, older, visibleNot
notifObj.image = status.attachments[0].url notifObj.image = status.attachments[0].url
} }
if (!notification.seen && !state.notifications.desktopNotificationSilence && visibleNotificationTypes.includes(notification.type)) { const reasonsToMuteNotif = (
notification.seen ||
state.notifications.desktopNotificationSilence ||
!visibleNotificationTypes.includes(notification.type) ||
(
notification.type === 'mention' && status && (
status.muted ||
muteWordHits(status, rootGetters.mergedConfig.muteWords).length === 0
)
)
)
if (!reasonsToMuteNotif) {
let desktopNotification = 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.

View file

@ -538,9 +538,11 @@ const fetchTimeline = ({
if (timeline === 'public' || timeline === 'publicAndExternal') { if (timeline === 'public' || timeline === 'publicAndExternal') {
params.push(['only_media', false]) params.push(['only_media', false])
} }
if (timeline !== 'favorites') {
params.push(['with_muted', withMuted])
}
params.push(['limit', 20]) params.push(['limit', 20])
params.push(['with_muted', withMuted])
const queryString = map(params, (param) => `${param[0]}=${param[1]}`).join('&') const queryString = map(params, (param) => `${param[0]}=${param[1]}`).join('&')
url += `?${queryString}` url += `?${queryString}`

View file

@ -210,7 +210,7 @@ export const addEmojis = (string, emojis) => {
const regexSafeShortCode = emoji.shortcode.replace(matchOperatorsRegex, '\\$&') const regexSafeShortCode = emoji.shortcode.replace(matchOperatorsRegex, '\\$&')
return acc.replace( return acc.replace(
new RegExp(`:${regexSafeShortCode}:`, 'g'), new RegExp(`:${regexSafeShortCode}:`, 'g'),
`<img src='${emoji.url}' alt='${emoji.shortcode}' title='${emoji.shortcode}' class='emoji' />` `<img src='${emoji.url}' alt=':${emoji.shortcode}:' title=':${emoji.shortcode}:' class='emoji' />`
) )
}, string) }, string)
} }

View file

@ -1,3 +1,4 @@
import { filter } from 'lodash'
import sanitize from 'sanitize-html' import sanitize from 'sanitize-html'
export const removeAttachmentLinks = (html) => { export const removeAttachmentLinks = (html) => {
@ -12,4 +13,14 @@ export const parse = (html) => {
return removeAttachmentLinks(html) return removeAttachmentLinks(html)
} }
export const muteWordHits = (status, muteWords) => {
const statusText = status.text.toLowerCase()
const statusSummary = status.summary.toLowerCase()
const hits = filter(muteWords, (muteWord) => {
return statusText.includes(muteWord.toLowerCase()) || statusSummary.includes(muteWord.toLowerCase())
})
return hits
}
export default parse export default parse

View file

@ -338,9 +338,9 @@ describe('API Entities normalizer', () => {
describe('MastoAPI emoji adder', () => { describe('MastoAPI emoji adder', () => {
const emojis = makeMockEmojiMasto() const emojis = makeMockEmojiMasto()
const imageHtml = '<img src="https://example.com/image.png" alt="image" title="image" class="emoji" />' const imageHtml = '<img src="https://example.com/image.png" alt=":image:" title=":image:" class="emoji" />'
.replace(/"/g, '\'') .replace(/"/g, '\'')
const thinkHtml = '<img src="https://example.com/think.png" alt="thinking" title="thinking" class="emoji" />' const thinkHtml = '<img src="https://example.com/think.png" alt=":thinking:" title=":thinking:" class="emoji" />'
.replace(/"/g, '\'') .replace(/"/g, '\'')
it('correctly replaces shortcodes in supplied string', () => { it('correctly replaces shortcodes in supplied string', () => {
@ -366,8 +366,8 @@ describe('API Entities normalizer', () => {
shortcode: '[a-z] {|}*' shortcode: '[a-z] {|}*'
}]) }])
const result = addEmojis('This post has :c++: emoji and :[a-z] {|}*: emoji', emojis) const result = addEmojis('This post has :c++: emoji and :[a-z] {|}*: emoji', emojis)
expect(result).to.include('title=\'c++\'') expect(result).to.include('title=\':c++:\'')
expect(result).to.include('title=\'[a-z] {|}*\'') expect(result).to.include('title=\':[a-z] {|}*:\'')
}) })
}) })
}) })