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:
commit
9e3e6b0c69
|
@ -6,6 +6,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
|||
### Changed
|
||||
- Removed the use of with_move parameters when fetching notifications
|
||||
|
||||
### Fixed
|
||||
- Multiple issues with muted statuses/notifications
|
||||
|
||||
## [Unreleased patch]
|
||||
### Add
|
||||
- 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
|
||||
- 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
|
||||
- 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
|
||||
### Fixed
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
@ -11,7 +11,6 @@ To translate Pleroma-FE, add your language to [src/i18n/messages.js](https://git
|
|||
# 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.
|
||||
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
|
||||
|
||||
|
|
|
@ -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.
|
||||
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.
|
||||
* **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:
|
||||
|
||||
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.
|
||||
|
|
|
@ -110,6 +110,7 @@ const setSettings = async ({ apiConfig, staticConfig, store }) => {
|
|||
copyInstanceOption('alwaysShowSubjectInput')
|
||||
copyInstanceOption('showFeaturesPanel')
|
||||
copyInstanceOption('hideSitename')
|
||||
copyInstanceOption('sidebarRight')
|
||||
|
||||
return store.dispatch('setTheme', config['theme'])
|
||||
}
|
||||
|
|
|
@ -84,10 +84,12 @@ const MediaModal = {
|
|||
}
|
||||
},
|
||||
mounted () {
|
||||
window.addEventListener('popstate', this.hide)
|
||||
document.addEventListener('keyup', this.handleKeyupEvent)
|
||||
document.addEventListener('keydown', this.handleKeydownEvent)
|
||||
},
|
||||
destroyed () {
|
||||
window.removeEventListener('popstate', this.hide)
|
||||
document.removeEventListener('keyup', this.handleKeyupEvent)
|
||||
document.removeEventListener('keydown', this.handleKeydownEvent)
|
||||
}
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import StatusContent from '../status_content/status_content.vue'
|
||||
import Status from '../status/status.vue'
|
||||
import UserAvatar from '../user_avatar/user_avatar.vue'
|
||||
import UserCard from '../user_card/user_card.vue'
|
||||
|
@ -16,10 +17,11 @@ const Notification = {
|
|||
},
|
||||
props: [ 'notification' ],
|
||||
components: {
|
||||
Status,
|
||||
StatusContent,
|
||||
UserAvatar,
|
||||
UserCard,
|
||||
Timeago
|
||||
Timeago,
|
||||
Status
|
||||
},
|
||||
methods: {
|
||||
toggleUserExpanded () {
|
||||
|
|
|
@ -157,11 +157,9 @@
|
|||
</router-link>
|
||||
</div>
|
||||
<template v-else>
|
||||
<status
|
||||
<status-content
|
||||
class="faint"
|
||||
:compact="true"
|
||||
:statusoid="notification.action"
|
||||
:no-heading="true"
|
||||
:status="notification.action"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
|
|
|
@ -36,6 +36,8 @@
|
|||
border-bottom: 1px solid;
|
||||
border-color: $fallback--border;
|
||||
border-color: var(--border, $fallback--border);
|
||||
word-wrap: break-word;
|
||||
word-break: break-word;
|
||||
|
||||
&:hover .animated.avatar {
|
||||
canvas {
|
||||
|
@ -46,10 +48,6 @@
|
|||
}
|
||||
}
|
||||
|
||||
.muted {
|
||||
padding: .25em .6em;
|
||||
}
|
||||
|
||||
.non-mention {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
|
|
|
@ -24,7 +24,7 @@ const ReactButton = {
|
|||
},
|
||||
computed: {
|
||||
commonEmojis () {
|
||||
return ['❤️', '😠', '👀', '😂', '🔥']
|
||||
return ['👍', '😠', '👀', '😂', '🔥']
|
||||
},
|
||||
emojis () {
|
||||
if (this.filterWord !== '') {
|
||||
|
|
|
@ -1,13 +1,31 @@
|
|||
import { filter, trim } from 'lodash'
|
||||
import Checkbox from 'src/components/checkbox/checkbox.vue'
|
||||
|
||||
import SharedComputedObject from './helpers/shared_computed_object.js'
|
||||
|
||||
const FilteringTab = {
|
||||
data () {
|
||||
return {
|
||||
muteWordsStringLocal: this.$store.getters.mergedConfig.muteWords.join('\n')
|
||||
}
|
||||
},
|
||||
components: {
|
||||
Checkbox
|
||||
},
|
||||
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
|
||||
watch: {
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import { filter, trim } from 'lodash'
|
||||
import {
|
||||
instanceDefaultProperties,
|
||||
multiChoiceProperties,
|
||||
|
@ -38,15 +37,6 @@ const SharedComputedObject = () => ({
|
|||
}])
|
||||
.reduce((acc, [key, value]) => ({ ...acc, [key]: value }), {}),
|
||||
// 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: {
|
||||
get () { return this.$store.getters.mergedConfig.useStreamingApi },
|
||||
set (value) {
|
||||
|
|
|
@ -12,7 +12,8 @@ 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 { 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'
|
||||
|
||||
const Status = {
|
||||
|
@ -44,6 +45,12 @@ const Status = {
|
|||
muteWords () {
|
||||
return this.mergedConfig.muteWords
|
||||
},
|
||||
showReasonMutedThread () {
|
||||
return (
|
||||
this.status.thread_muted ||
|
||||
(this.status.reblog && this.status.reblog.thread_muted)
|
||||
) && !this.inConversation
|
||||
},
|
||||
repeaterClass () {
|
||||
const user = this.statusoid.user
|
||||
return highlightClass(user)
|
||||
|
@ -93,20 +100,42 @@ const Status = {
|
|||
return !!this.currentUser
|
||||
},
|
||||
muteWordHits () {
|
||||
const statusText = this.status.text.toLowerCase()
|
||||
const statusSummary = this.status.summary.toLowerCase()
|
||||
const hits = filter(this.muteWords, (muteWord) => {
|
||||
return statusText.includes(muteWord.toLowerCase()) || statusSummary.includes(muteWord.toLowerCase())
|
||||
})
|
||||
|
||||
return hits
|
||||
return muteWordHits(this.status, this.muteWords)
|
||||
},
|
||||
muted () {
|
||||
const relationship = this.$store.getters.relationship(this.status.user.id)
|
||||
return !this.unmuted && (
|
||||
(!(this.inProfile && this.status.user.id === this.profileUserId) && relationship.muting) ||
|
||||
(!this.inConversation && this.status.thread_muted) ||
|
||||
this.muteWordHits.length > 0)
|
||||
const { status } = this
|
||||
const { reblog } = status
|
||||
const relationship = this.$store.getters.relationship(status.user.id)
|
||||
const relationshipReblog = reblog && this.$store.getters.relationship(reblog.user.id)
|
||||
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 () {
|
||||
return this.mergedConfig.hideFilteredStatuses
|
||||
|
|
|
@ -17,12 +17,33 @@
|
|||
</div>
|
||||
<template v-if="muted && !isPreview">
|
||||
<div class="media status container muted">
|
||||
<small>
|
||||
<small class="username">
|
||||
<i
|
||||
v-if="muted && retweet"
|
||||
class="button-icon icon-retweet"
|
||||
/>
|
||||
<router-link :to="userProfileLink">
|
||||
{{ status.user.screen_name }}
|
||||
</router-link>
|
||||
</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
|
||||
href="#"
|
||||
class="unmute"
|
||||
|
@ -637,19 +658,48 @@ $status-margin: 0.75em;
|
|||
}
|
||||
|
||||
.muted {
|
||||
padding: 0.25em 0.5em;
|
||||
button {
|
||||
padding: .25em .6em;
|
||||
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;
|
||||
}
|
||||
|
||||
.muteWords {
|
||||
margin-left: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
a.unmute {
|
||||
display: block;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.reply-body {
|
||||
|
|
|
@ -176,8 +176,8 @@ const StatusContent = {
|
|||
}
|
||||
}
|
||||
if (target.rel.match(/(?:^|\s)tag(?:$|\s)/) || target.className.match(/hashtag/)) {
|
||||
// Extract tag name from link url
|
||||
const tag = extractTagFromUrl(target.href)
|
||||
// Extract tag name from dataset or link url
|
||||
const tag = target.dataset.tag || extractTagFromUrl(target.href)
|
||||
if (tag) {
|
||||
const link = this.generateTagLink(tag)
|
||||
this.$router.push(link)
|
||||
|
|
|
@ -23,12 +23,21 @@
|
|||
|
||||
<style lang="scss">
|
||||
@import '../../_variables.scss';
|
||||
.contain-fit {
|
||||
.still-image {
|
||||
img {
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.still-image {
|
||||
position: relative;
|
||||
line-height: 0;
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
|
||||
&:hover canvas {
|
||||
display: none;
|
||||
|
@ -36,8 +45,8 @@
|
|||
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
&.animated {
|
||||
|
|
|
@ -627,7 +627,9 @@
|
|||
"mute_conversation": "Mute conversation",
|
||||
"unmute_conversation": "Unmute conversation",
|
||||
"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": {
|
||||
"approve": "Approve",
|
||||
|
|
|
@ -15,7 +15,7 @@ import {
|
|||
import { set } from 'vue'
|
||||
import { isStatusNotification } from '../services/notification_utils/notification_utils.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) => ({
|
||||
statuses: [],
|
||||
|
@ -381,7 +381,18 @@ const addNewNotifications = (state, { dispatch, notifications, older, visibleNot
|
|||
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)
|
||||
// Chrome is known for not closing notifications automatically
|
||||
// according to MDN, anyway.
|
||||
|
|
|
@ -538,9 +538,11 @@ const fetchTimeline = ({
|
|||
if (timeline === 'public' || timeline === 'publicAndExternal') {
|
||||
params.push(['only_media', false])
|
||||
}
|
||||
if (timeline !== 'favorites') {
|
||||
params.push(['with_muted', withMuted])
|
||||
}
|
||||
|
||||
params.push(['limit', 20])
|
||||
params.push(['with_muted', withMuted])
|
||||
|
||||
const queryString = map(params, (param) => `${param[0]}=${param[1]}`).join('&')
|
||||
url += `?${queryString}`
|
||||
|
|
|
@ -210,7 +210,7 @@ export const addEmojis = (string, emojis) => {
|
|||
const regexSafeShortCode = emoji.shortcode.replace(matchOperatorsRegex, '\\$&')
|
||||
return acc.replace(
|
||||
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)
|
||||
}
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import { filter } from 'lodash'
|
||||
import sanitize from 'sanitize-html'
|
||||
|
||||
export const removeAttachmentLinks = (html) => {
|
||||
|
@ -12,4 +13,14 @@ export const parse = (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
|
||||
|
|
|
@ -338,9 +338,9 @@ describe('API Entities normalizer', () => {
|
|||
|
||||
describe('MastoAPI emoji adder', () => {
|
||||
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, '\'')
|
||||
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, '\'')
|
||||
|
||||
it('correctly replaces shortcodes in supplied string', () => {
|
||||
|
@ -366,8 +366,8 @@ describe('API Entities normalizer', () => {
|
|||
shortcode: '[a-z] {|}*'
|
||||
}])
|
||||
const result = addEmojis('This post has :c++: emoji and :[a-z] {|}*: emoji', emojis)
|
||||
expect(result).to.include('title=\'c++\'')
|
||||
expect(result).to.include('title=\'[a-z] {|}*\'')
|
||||
expect(result).to.include('title=\':c++:\'')
|
||||
expect(result).to.include('title=\':[a-z] {|}*:\'')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
Loading…
Reference in New Issue