Compare commits

...

58 Commits

Author SHA1 Message Date
Sam Therapy 440c2c15b6
PWA time
continuous-integration/drone/push Build is passing Details
2023-04-12 15:00:59 +02:00
Zero a6a77233c8
Restore "Keep picker open"
continuous-integration/drone/push Build is failing Details
2023-01-31 16:14:59 +01:00
yan cf92ca25e3
Reformat code with standardjs
Reformat code with standardjs as the majority of files seem to follow
that.
2023-01-31 16:14:48 +01:00
yan 23a3b1a8b3
Improve emoji picker performance
A simple virtual scroller is now used for the emoji grid. This avoids
loading all emoji images at once, saving network bandwidth and
reducing load on the server, while also putting less work on the
browser's DOM and layout engine.
2023-01-31 16:14:25 +01:00
Sam Therapy f30b28032d
Revert "Add virtual scrolling to emoji picker"
Use the newer one instead
This reverts commit 61fa33739d.
2023-01-30 21:21:25 +01:00
eris 8d391fd3d3
Add indicator if user blocks you
continuous-integration/drone/push Build is failing Details
2023-01-27 15:49:31 +01:00
Sam Therapy c02e757528
sodd lock
Signed-off-by: Sam Therapy <sam@samtherapy.net>
2023-01-27 15:43:58 +01:00
Zero 53516bea57
Update ESLint to a newer version 2023-01-27 15:41:55 +01:00
Zero 93ea1f0659
Implement loading more statuses when searching
continuous-integration/drone/push Build was killed Details
2023-01-24 23:07:51 +01:00
Sam Therapy 4241b67771
nothing
Signed-off-by: Sam Therapy <sam@samtherapy.net>
2023-01-24 23:07:45 +01:00
Sam Therapy 61fa33739d
Add virtual scrolling to emoji picker
continuous-integration/drone/push Build is failing Details
Signed-off-by: Sam Therapy <sam@samtherapy.net>
2023-01-24 22:32:52 +01:00
Sam Therapy c64a627438
add CI stuff + yarn 2
continuous-integration/drone/push Build is failing Details
Signed-off-by: Sam Therapy <sam@samtherapy.net>
2023-01-20 22:53:31 +01:00
Sam Therapy b29f9c4e88
fix user relationship url
Signed-off-by: Sam Therapy <sam@samtherapy.net>
2023-01-20 22:52:49 +01:00
FloatingGhost 74b1e66dc3 don't crash out if notification status is null
7ff17ab722
2022-12-20 15:37:01 -05:00
Zero 99214ce111 Allow to click external reacts (seem to work for misskey) 2022-12-18 22:06:06 -05:00
Zero 8f99ca066a annoying shit 2022-12-15 17:43:40 -05:00
Zero 010675cb80 Use /v1/ URLs (Akkoma change) 2022-12-15 17:38:50 -05:00
Zero 4176ef4996 Steal just the updated emoji picker from Akkoma's pleroma-fe 2022-12-10 08:49:00 -05:00
Zero 2745aabcb4 Merge branch 'custom_reacts' into stable (edwin squirty) 2022-12-01 17:54:36 -05:00
Zero 72efb7d0e9 Fix picker not staying open for reacts (also thanks samme) 2022-12-01 17:34:19 -05:00
Zero cc09f0bcb9 Fix broken picker for react menu (thanks samme) 2022-12-01 17:30:12 -05:00
floatingghost 655adfdbd8 Reuse the emoji picker for reactions (#7)
Fixes #6

Co-authored-by: FloatingGhost <hannah@coffee-and-dreams.uk>
Reviewed-on: http://akkoma.dev/AkkomaGang/pleroma-fe/pulls/7
2022-12-01 16:40:04 -05:00
Zero 8cff52ac32 Revert "Add a super basic keep reaction open button"
This reverts commit d8ec9f596d.
2022-12-01 16:40:04 -05:00
Zero 8d3d8de7a3 Better CSS for reacts 2022-12-01 16:40:04 -05:00
FloatingGhost a6a5f03966 make emoji reactions in notifications bar wider 2022-12-01 15:21:29 -05:00
Sol Fisher Romanoff 83fa4ad4a7 Highlight emoji reactions picked by yourself 2022-12-01 15:21:28 -05:00
FloatingGhost 0ab60d63ef fix "who reacted" emoji display
fixes #80
2022-12-01 15:21:28 -05:00
eris 788691b296 Better formatting for emoji react notifications 2022-12-01 15:21:28 -05:00
FloatingGhost fc7cdb1e69 fix css 2022-12-01 14:23:03 -05:00
FloatingGhost 44047458e4 add custom emoji display 2022-12-01 14:22:14 -05:00
Sol Fisher Romanoff c27845169d Add emoji name as title tag 2022-12-01 14:18:49 -05:00
sadposter 378ac642d4 fix width 2022-12-01 14:17:55 -05:00
FloatingGhost 23b60e9263 fix emoji sizing 2022-12-01 14:17:12 -05:00
FloatingGhost e1785d2d91 force width 2022-12-01 14:15:23 -05:00
FloatingGhost 3cc6a0b252 allow custom reactions 2022-12-01 14:15:03 -05:00
FloatingGhost 710891d1f3 allow customs 2022-12-01 14:13:57 -05:00
Zero 6170aa3ee7 Update package versions
Vue2 to the latest version manually and the rest with `yarn upgrade`
2022-11-30 15:13:08 -05:00
Zero afb388b73b Add Unicode 15.0 emojis to the FE 2022-11-29 09:45:17 -05:00
Zero 6729ab8cf1 Add every Unicode 14.0 emoji to the FE 2022-02-24 15:23:52 -05:00
Zero 69671446e5 Add more default reacts 2022-02-23 16:00:03 -05:00
Sam Therapy d8ec9f596d Add a super basic keep reaction open button
Signed-off-by: Sam Therapy <sam@samtherapy.net>
2022-02-19 19:02:39 -05:00
Zero df8bf08f46 Revert "entity_normalizer: Escape name when parsing user"
This reverts commit e32ae82441.
2022-02-19 18:16:11 -05:00
Sam Therapy 2d43adb6e5 Change default emoji reacts Why
Signed-off-by: Sam Therapy <sam@samtherapy.net>
2022-02-19 17:44:18 -05:00
Zero 402f3a61fe Add emoji.json generated from Pleroma's emoji-test.txt 2022-02-19 17:40:33 -05:00
Tusooa Zhu 58cd2fb994 Fix Follow button missing on follow list 2022-02-05 16:08:34 -05:00
Zero e32ae82441 entity_normalizer: Escape name when parsing user
In January 2020 Pleroma backend stopped escaping HTML in display names
and passed that responsibility on frontends, compliant with Mastodon's
version of Mastodon API [1]. Pleroma-FE was subsequently modified to
escape the display name [2], however only in the "name_html" field. This
was fine however, since that's what the code rendering display names used.

However, 2 months ago an MR [3] refactoring the way the frontend does emoji
and mention rendering was merged. One of the things it did was moving away
from doing emoji rendering in the entity normalizer and use the unescaped
'user.name' in the rendering code, resulting in HTML injection being
possible again.

This patch escapes 'user.name' as well, as far as I can tell there is no
actual use for an unescaped display name in frontend code, especially
when it comes from MastoAPI, where it is not supposed to be HTML.

d36b45ad43
2022-01-26 20:07:11 -05:00
Zero 0f5e601db2 Revert "Fix sidebar scroll height"
This reverts commit fbd9ee03c4.
2022-01-26 18:26:55 -05:00
Zero 2f215b8d2e oh nyo 2022-01-26 18:19:54 -05:00
Zero 9c827c9834 Revert "Add timeline menu toggle to nav panel"
This reverts commit 98cb9abac7.
2022-01-26 15:54:29 -05:00
Zero 7622b1230f Simple policy reasons for instance specific policies
a20f1794d0
2022-01-26 09:45:48 -05:00
Ilja 84ce0046f9 Improve the user card for deactivated users 2022-01-25 15:49:18 -05:00
Zero 99d2c75a1a Update package.json
Update yarn.lock
2022-01-25 15:35:21 -05:00
Zero fbd9ee03c4 Fix sidebar scroll height 2022-01-25 15:14:23 -05:00
Zero 387958c7f1 Remove annoying error messages lole 2022-01-25 15:14:02 -05:00
Zero 51cb9b5c8f Change some default strings 2022-01-25 15:13:31 -05:00
Zero 16772d75f7 Add puniko's third column w/ compatibility for right sidebar 2022-01-25 15:12:40 -05:00
Ilja 241d520795 Allow canceling a follow request
When a follow request is sent, but not (yet) accepted, the behaviour is now to cancel the request instead of re sending.

The reason is double
* You couldn't cancel a follow request if you change your mind and the request wasn't answered yet
* Instances don't always correctly process a new follow request when the following is already happening. If something went wrong (e;g. the target server thinks you're following, but your instance thinks you're not yet), it's better to first sent an unfollow. This is the behaviour that Mastodon and most probably most other clients have. Therefore this flow is more tested and expected by other instances.
2022-01-25 15:11:19 -05:00
Henry Jameson 2ce47e8cf0 fix ext profile bug 2022-01-25 15:03:31 -05:00
75 changed files with 19206 additions and 11398 deletions

42
.drone.yml Normal file
View File

@ -0,0 +1,42 @@
kind: pipeline
type: docker
name: Deploy
clone:
disable: true
steps:
- name: Clone
image: woodpeckerci/plugin-git
settings:
recursive: true
- name: Build
depends_on:
- Clone
image: node:16
commands:
- yarn
- yarn build
when:
event:
- push
- name: Execute deploy script
depends_on:
- Build
image: ubuntu:latest
environment:
SSH_KEY:
from_secret: SSH_KEY
commands:
- apt update && apt install -y openssh-client rsync
- ./ci/add-key.sh
- ./ci/deploy.sh
when:
event:
- push
branch:
- froth
- froth-akkoma
- froth-zero

View File

@ -2,7 +2,8 @@ module.exports = {
root: true,
parserOptions: {
parser: 'babel-eslint',
sourceType: 'module'
sourceType: 'module',
"ecmaVersion": 2020
},
// https://github.com/feross/standard/blob/master/RULES.md#javascript-standard-style
extends: [
@ -21,6 +22,17 @@ module.exports = {
'generator-star-spacing': 0,
// allow debugger during development
'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0,
'vue/require-prop-types': 0
'vue/attribute-hyphenation': 'off',
'vue/attributes-order': 'off',
'vue/component-definition-name-casing': 'off',
'vue/component-tags-order': 'off',
'vue/html-closing-bracket-spacing': 'off',
'vue/html-indent': 'off',
'vue/multi-word-component-names': 'off',
'vue/no-lone-template': 'off',
'vue/no-reserved-component-names': 'off',
'vue/no-v-text-v-html-on-component': 'off',
'vue/require-prop-types': 0,
'vue/v-slot-style': 'off'
}
}

10
.gitignore vendored
View File

@ -7,3 +7,13 @@ test/e2e/reports
selenium-debug.log
.idea/
config/local.json
.dccache
.pnp.*
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/sdks
!.yarn/versions

1
.yarnrc.yml Normal file
View File

@ -0,0 +1 @@
nodeLinker: node-modules

View File

@ -27,7 +27,6 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
### Changed
- Display 'people voted' instead of 'votes' for multi-choice polls
- Changed the "Timelines" link in side panel to toggle show all timeline options inside the panel
- Renamed "Timeline" to "Home Timeline" to be more clear
- Optimized chat to not get horrible performance after keeping the same chat open for a long time
- When opening emoji picker or react picker, it automatically focuses the search field

17
ci/add-key.sh Executable file
View File

@ -0,0 +1,17 @@
#!/bin/sh
# only execute this script as part of the pipeline.
[ -z "$CI" ] && echo "missing ci environment variable" && exit 2
# only execute the script when github token exists.
[ -z "$SSH_KEY" ] && echo "missing ssh key" && exit 3
# write the ssh key.
mkdir /root/.ssh
echo -n "${SSH_KEY}" > /root/.ssh/id_ed25519
chmod 600 /root/.ssh/id_ed25519
# add froth.zone to our known hosts.
touch /root/.ssh/known_hosts
chmod 600 /root/.ssh/known_hosts
ssh-keyscan -H froth.zone > /etc/ssh/ssh_known_hosts 2> /dev/null

11
ci/deploy.sh Executable file
View File

@ -0,0 +1,11 @@
#!/usr/bin/env bash
TARGET="pleroma@froth.zone:/var/www/pleroma/zero"
#rsync -ra public/ "${TARGET}/instance/static"
#cp dist/index.html "${TARGET}/instance/static/index.html"
rsync --update --delete -Pr dist/ "${TARGET}"
#rsync --update -ra dist/static/ "${TARGET}/instance/static/static"
#rsync --delete -ra images/ "${TARGET}/instance/static/images"
#rsync --delete -ra sounds/ "${TARGET}/instance/static/sounds"
#rsync -ra instance/ "${TARGET}/instance/static/instance"
#rsync --delete -ra pages/ "${TARGET}/instance/static/pages"

View File

@ -5,6 +5,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1,user-scalable=no">
<!--server-generated-meta-->
<link rel="icon" type="image/png" href="/favicon.png">
<link rel="manifest" href="/manifest.json">
</head>
<body class="hidden">
<noscript>To use Pleroma, please enable JavaScript.</noscript>

View File

@ -34,7 +34,7 @@
"punycode.js": "^2.1.0",
"ruffle-mirror": "^2021.4.10",
"v-click-outside": "^2.1.1",
"vue": "^2.6.11",
"vue": "^2.7.14",
"vue-i18n": "^7.3.2",
"vue-router": "^3.0.1",
"vue-template-compiler": "^2.6.11",
@ -47,11 +47,11 @@
"@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/babel-helper-vue-jsx-merge-props": "^1.2.1",
"@vue/babel-preset-jsx": "^1.2.4",
"@vue/test-utils": "^1.0.0-beta.26",
"autoprefixer": "^6.4.0",
"babel-eslint": "^7.0.0",
"babel-eslint": "^10.1.0",
"babel-loader": "^8.0.6",
"babel-plugin-lodash": "^3.3.4",
"chai": "^3.5.0",
@ -62,15 +62,15 @@
"cross-spawn": "^4.0.2",
"css-loader": "^0.28.0",
"custom-event-polyfill": "^1.0.7",
"eslint": "^5.16.0",
"eslint": "^7.32.0",
"eslint-config-standard": "^12.0.0",
"eslint-friendly-formatter": "^2.0.5",
"eslint-loader": "^2.1.0",
"eslint-loader": "^4.0.2",
"eslint-plugin-import": "^2.13.0",
"eslint-plugin-node": "^7.0.0",
"eslint-plugin-promise": "^4.0.0",
"eslint-plugin-standard": "^4.0.0",
"eslint-plugin-vue": "^5.2.2",
"eslint-plugin-vue": "^9.9.0",
"eventsource-polyfill": "^0.9.6",
"express": "^4.13.3",
"file-loader": "^3.0.1",
@ -100,7 +100,7 @@
"postcss-loader": "^3.0.0",
"raw-loader": "^0.5.1",
"sass": "^1.17.3",
"sass-loader": "git://github.com/webpack-contrib/sass-loader",
"sass-loader": "^10",
"selenium-server": "2.53.1",
"semver": "^5.3.0",
"serviceworker-webpack-plugin": "^1.0.0",
@ -121,5 +121,8 @@
"engines": {
"node": ">= 4.0.0",
"npm": ">= 3.0.0"
},
"volta": {
"node": "16.20.0"
}
}

View File

@ -72,6 +72,9 @@ export default {
!this.$store.getters.mergedConfig.hideISP &&
this.$store.state.instance.instanceSpecificPanelContent
},
thirdColumnEnabled () {
return this.$store.getters.mergedConfig.showThirdColumn || false
},
showFeaturesPanel () { return this.$store.state.instance.showFeaturesPanel },
hideShoutbox () {
return this.$store.getters.mergedConfig.hideShoutbox
@ -83,6 +86,16 @@ export default {
'order': this.$store.getters.mergedConfig.sidebarRight ? 99 : 0
}
},
notifsAlign () {
return {
'order': this.$store.getters.mergedConfig.sidebarRight ? 0 : 99
}
},
thirdColumnLayout () {
return {
'max-width': this.$store.getters.mergedConfig.showThirdColumn ? '1400px' : '980px'
}
},
...mapGetters(['mergedConfig'])
},
methods: {

View File

@ -573,9 +573,10 @@ nav {
}
.main {
flex-basis: 50%;
flex-basis: 25%;
flex-grow: 1;
flex-shrink: 1;
order: 50;
}
.sidebar-bounds {

View File

@ -12,6 +12,7 @@
<div class="app-bg-wrapper app-container-wrapper" />
<div
id="content"
:style="thirdColumnLayout"
class="container underlay"
>
<div
@ -27,13 +28,15 @@
<instance-specific-panel v-if="showInstanceSpecificPanel" />
<features-panel v-if="!currentUser && showFeaturesPanel" />
<who-to-follow-panel v-if="currentUser && suggestionsEnabled" />
<notifications v-if="currentUser" />
<notifications v-if="currentUser && !thirdColumnEnabled" />
</div>
</div>
</div>
</div>
</div>
<div class="main">
<div
class="main"
>
<div
v-if="!currentUser"
class="login-hint panel panel-default"
@ -47,6 +50,23 @@
</div>
<router-view />
</div>
<div
v-if="thirdColumnEnabled"
class="sidebar-flexer mobile-hidden"
:style="notifsAlign"
>
<div class="sidebar-bounds">
<div class="sidebar-scroller">
<div class="sidebar">
<div v-if="!isMobileLayout">
<notifications v-if="currentUser" />
<features-panel v-if="!currentUser && showFeaturesPanel" />
</div>
</div>
</div>
</div>
</div>
<media-modal />
</div>
<shout-panel

View File

@ -0,0 +1,133 @@
const EMOJI_SIZE = 32 + 8
const GROUP_TITLE_HEIGHT = 24
const BUFFER_SIZE = 3 * EMOJI_SIZE
const EmojiGrid = {
props: {
groups: {
required: true,
type: Array
}
},
data () {
return {
containerWidth: 0,
containerHeight: 0,
scrollPos: 0,
resizeObserver: null
}
},
mounted () {
const rect = this.$refs.container.getBoundingClientRect()
this.containerWidth = rect.width
this.containerHeight = rect.height
this.resizeObserver = new ResizeObserver((entries) => {
for (const entry of entries) {
this.containerWidth = entry.contentRect.width
this.containerHeight = entry.contentRect.height
}
})
this.resizeObserver.observe(this.$refs.container)
},
beforeUnmount () {
this.resizeObserver.disconnect()
this.resizeObserver = null
},
watch: {
groups () {
// Scroll to top when grid content changes
if (this.$refs.container) {
this.$refs.container.scrollTo(0, 0)
}
},
activeGroup (group) {
this.$emit('activeGroup', group)
}
},
methods: {
onScroll () {
this.scrollPos = this.$refs.container.scrollTop
},
onEmoji (emoji) {
this.$emit('emoji', emoji)
},
scrollToItem (itemId) {
const container = this.$refs.container
if (!container) return
for (const item of this.itemList) {
if (item.id === itemId) {
container.scrollTo(0, item.position.y)
return
}
}
}
},
computed: {
// Total height of scroller content
gridHeight () {
if (this.itemList.length === 0) return 0
const lastItem = this.itemList[this.itemList.length - 1]
return (
lastItem.position.y +
('title' in lastItem ? GROUP_TITLE_HEIGHT : EMOJI_SIZE)
)
},
activeGroup () {
const items = this.itemList
for (let i = items.length - 1; i >= 0; i--) {
const item = items[i]
if ('title' in item && item.position.y <= this.scrollPos) {
return item.id
}
}
return null
},
itemList () {
const items = []
let x = 0
let y = 0
for (const group of this.groups) {
items.push({ position: { x, y }, id: group.id, title: group.text })
if (group.text.length) {
y += GROUP_TITLE_HEIGHT
}
for (const emoji of group.emojis) {
items.push({
position: { x, y },
id: `${group.id}-${emoji.displayText}`,
emoji
})
x += EMOJI_SIZE
if (x + EMOJI_SIZE > this.containerWidth) {
y += EMOJI_SIZE
x = 0
}
}
if (x > 0) {
y += EMOJI_SIZE
x = 0
}
}
return items
},
visibleItems () {
const startPos = this.scrollPos - BUFFER_SIZE
const endPos = this.scrollPos + this.containerHeight + BUFFER_SIZE
return this.itemList.filter((i) => {
return i.position.y >= startPos && i.position.y < endPos
})
},
scrolledClass () {
if (this.scrollPos <= 5) {
return 'scrolled-top'
} else if (this.scrollPos >= this.gridHeight - this.containerHeight - 5) {
return 'scrolled-bottom'
} else {
return 'scrolled-middle'
}
}
}
}
export default EmojiGrid

View File

@ -0,0 +1,60 @@
.emoji {
&-grid {
flex: 1 1 1px;
position: relative;
overflow: auto;
user-select: none;
mask: linear-gradient(to top, white 0, transparent 100%) bottom no-repeat,
linear-gradient(to bottom, white 0, transparent 100%) top no-repeat,
linear-gradient(to top, white, white);
transition: mask-size 150ms;
mask-size: 100% 20px, 100% 20px, auto;
// Autoprefixed seem to ignore this one, and also syntax is different
-webkit-mask-composite: xor;
mask-composite: exclude;
&.scrolled {
&-top {
mask-size: 100% 20px, 100% 0, auto;
}
&-bottom {
mask-size: 100% 0, 100% 20px, auto;
}
}
margin-left: 5px;
min-height: 200px;
}
&-group-title {
position: absolute;
font-size: 0.85em;
width: 100%;
margin: 0;
height: 24px;
display: flex;
align-items: end;
&.disabled {
display: none;
}
}
&-item {
position: absolute;
width: 32px;
height: 32px;
box-sizing: border-box;
display: flex;
font-size: 32px;
align-items: center;
justify-content: center;
margin: 4px;
cursor: pointer;
img {
object-fit: contain;
max-width: 100%;
max-height: 100%;
}
}
}

View File

@ -0,0 +1,48 @@
<template>
<div
ref="container"
class="emoji-grid"
:class="scrolledClass"
@scroll.passive="onScroll"
>
<div
:style="{
height: `${gridHeight}px`,
}"
>
<template v-for="item in visibleItems">
<h6
v-if="'title' in item && item.title.length"
:key="'title-' + item.id"
class="emoji-group-title"
:style="{
top: item.position.y + 'px',
left: item.position.x + 'px'
}"
>
{{ item.title }}
</h6>
<span
v-else-if="'emoji' in item"
:key="'emoji-' + item.id"
class="emoji-item"
:title="item.emoji.displayText"
:style="{
top: item.position.y + 'px',
left: item.position.x + 'px'
}"
@click.stop.prevent="onEmoji(item.emoji)"
>
<span v-if="!item.emoji.imageUrl">{{ item.emoji.replacement }}</span>
<img
v-else
:src="item.emoji.imageUrl"
>
</span>
</template>
</div>
</div>
</template>
<script src="./emoji_grid.js"></script>
<style lang="scss" src="./emoji_grid.scss"></style>

View File

@ -202,7 +202,6 @@ const EmojiInput = {
},
triggerShowPicker () {
this.showPicker = true
this.$refs.picker.startEmojiLoad()
this.$nextTick(() => {
this.scrollIntoView()
this.focusPickerInput()
@ -220,7 +219,6 @@ const EmojiInput = {
this.showPicker = !this.showPicker
if (this.showPicker) {
this.scrollIntoView()
this.$refs.picker.startEmojiLoad()
this.$nextTick(this.focusPickerInput)
}
},

View File

@ -1,10 +1,13 @@
import { defineAsyncComponent } from 'vue'
import Checkbox from '../checkbox/checkbox.vue'
import EmojiGrid from '../emoji_grid/emoji_grid.vue'
import { library } from '@fortawesome/fontawesome-svg-core'
import {
faBoxOpen,
faStickyNote,
faSmileBeam
} from '@fortawesome/free-solid-svg-icons'
import { trim, escapeRegExp, startCase } from 'lodash'
library.add(
faBoxOpen,
@ -12,30 +15,6 @@ library.add(
faSmileBeam
)
// At widest, approximately 20 emoji are visible in a row,
// loading 3 rows, could be overkill for narrow picker
const LOAD_EMOJI_BY = 60
// When to start loading new batch emoji, in pixels
const LOAD_EMOJI_MARGIN = 64
const filterByKeyword = (list, keyword = '') => {
if (keyword === '') return list
const keywordLowercase = keyword.toLowerCase()
let orderedEmojiList = []
for (const emoji of list) {
const indexOfKeyword = emoji.displayText.toLowerCase().indexOf(keywordLowercase)
if (indexOfKeyword > -1) {
if (!Array.isArray(orderedEmojiList[indexOfKeyword])) {
orderedEmojiList[indexOfKeyword] = []
}
orderedEmojiList[indexOfKeyword].push(emoji)
}
}
return orderedEmojiList.flat()
}
const EmojiPicker = {
props: {
enableStickerPicker: {
@ -47,18 +26,15 @@ const EmojiPicker = {
data () {
return {
keyword: '',
activeGroup: 'custom',
activeGroup: 'standard',
showingStickers: false,
groupsScrolledClass: 'scrolled-top',
keepOpen: false,
customEmojiBufferSlice: LOAD_EMOJI_BY,
customEmojiTimeout: null,
customEmojiLoadAllConfirmed: false
keepOpen: false
}
},
components: {
StickerPicker: () => import('../sticker_picker/sticker_picker.vue'),
Checkbox
StickerPicker: defineAsyncComponent(() => import('../sticker_picker/sticker_picker.vue')),
Checkbox,
EmojiGrid
},
methods: {
onStickerUploaded (e) {
@ -71,95 +47,32 @@ const EmojiPicker = {
const value = emoji.imageUrl ? `:${emoji.displayText}:` : emoji.replacement
this.$emit('emoji', { insertion: value, keepOpen: this.keepOpen })
},
onScroll (e) {
const target = (e && e.target) || this.$refs['emoji-groups']
this.updateScrolledClass(target)
this.scrolledGroup(target)
this.triggerLoadMore(target)
onWheel (e) {
e.preventDefault()
this.$refs['emoji-tabs'].scrollBy(e.deltaY, 0)
},
highlight (key) {
const ref = this.$refs['group-' + key]
const top = ref[0].offsetTop
this.setShowStickers(false)
this.activeGroup = key
this.$nextTick(() => {
this.$refs['emoji-groups'].scrollTop = top + 1
})
},
updateScrolledClass (target) {
if (target.scrollTop <= 5) {
this.groupsScrolledClass = 'scrolled-top'
} else if (target.scrollTop >= target.scrollTopMax - 5) {
this.groupsScrolledClass = 'scrolled-bottom'
} else {
this.groupsScrolledClass = 'scrolled-middle'
if (this.keyword.length) {
this.$refs.emojiGrid.scrollToItem(key)
}
},
triggerLoadMore (target) {
const ref = this.$refs['group-end-custom'][0]
if (!ref) return
const bottom = ref.offsetTop + ref.offsetHeight
const scrollerBottom = target.scrollTop + target.clientHeight
const scrollerTop = target.scrollTop
const scrollerMax = target.scrollHeight
// Loads more emoji when they come into view
const approachingBottom = bottom - scrollerBottom < LOAD_EMOJI_MARGIN
// Always load when at the very top in case there's no scroll space yet
const atTop = scrollerTop < 5
// Don't load when looking at unicode category or at the very bottom
const bottomAboveViewport = bottom < scrollerTop || scrollerBottom === scrollerMax
if (!bottomAboveViewport && (approachingBottom || atTop)) {
this.loadEmoji()
}
},
scrolledGroup (target) {
const top = target.scrollTop + 5
this.$nextTick(() => {
this.emojisView.forEach(group => {
const ref = this.$refs['group-' + group.id]
if (ref[0].offsetTop <= top) {
this.activeGroup = group.id
}
})
})
},
loadEmoji () {
const allLoaded = this.customEmojiBuffer.length === this.filteredEmoji.length
if (allLoaded) {
return
}
this.customEmojiBufferSlice += LOAD_EMOJI_BY
},
startEmojiLoad (forceUpdate = false) {
if (!forceUpdate) {
this.keyword = ''
}
this.$nextTick(() => {
this.$refs['emoji-groups'].scrollTop = 0
})
const bufferSize = this.customEmojiBuffer.length
const bufferPrefilledAll = bufferSize === this.filteredEmoji.length
if (bufferPrefilledAll && !forceUpdate) {
return
}
this.customEmojiBufferSlice = LOAD_EMOJI_BY
onActiveGroup (group) {
this.activeGroup = group
},
toggleStickers () {
this.showingStickers = !this.showingStickers
},
setShowStickers (value) {
this.showingStickers = value
}
},
watch: {
keyword () {
this.customEmojiLoadAllConfirmed = false
this.onScroll()
this.startEmojiLoad(true)
},
filterByKeyword (list) {
if (this.keyword === '') return list
const regex = new RegExp(escapeRegExp(trim(this.keyword)), 'i')
return list.filter(emoji => {
return (regex.test(emoji.displayText) || (!emoji.imageUrl && emoji.replacement === this.keyword))
})
}
},
computed: {
@ -173,38 +86,59 @@ const EmojiPicker = {
return 0
},
filteredEmoji () {
return filterByKeyword(
this.$store.state.instance.customEmoji || [],
this.keyword
return this.filterByKeyword(
this.$store.state.instance.customEmoji || []
)
},
customEmojiBuffer () {
return this.filteredEmoji.slice(0, this.customEmojiBufferSlice)
},
emojis () {
const standardEmojis = this.$store.state.instance.emoji || []
const customEmojis = this.customEmojiBuffer
const customEmojis = this.sortedEmoji
const emojiPacks = []
customEmojis.forEach((pack, id) => {
emojiPacks.push({
id: id.replace(/^pack:/, ''),
text: startCase(id.replace(/^pack:/, '')),
first: pack[0],
emojis: this.filterByKeyword(pack)
})
})
return [
{
id: 'custom',
text: this.$t('emoji.custom'),
icon: 'smile-beam',
emojis: customEmojis
},
{
id: 'standard',
text: this.$t('emoji.unicode'),
icon: 'box-open',
emojis: filterByKeyword(standardEmojis, this.keyword)
first: {
imageUrl: '',
replacement: '🥴'
},
emojis: this.filterByKeyword(standardEmojis)
}
]
].concat(emojiPacks)
},
sortedEmoji () {
const customEmojis = this.$store.state.instance.customEmoji || []
const sortedEmojiGroups = new Map()
customEmojis.forEach((emoji) => {
if (!sortedEmojiGroups.has(emoji.tags[0])) {
sortedEmojiGroups.set(emoji.tags[0], [emoji])
} else {
sortedEmojiGroups.get(emoji.tags[0]).push(emoji)
}
})
return new Map([...sortedEmojiGroups.entries()].sort())
},
emojisView () {
return this.emojis.filter(value => value.emojis.length > 0)
if (this.keyword === '') {
return this.emojis.filter(pack => {
return pack.id === this.activeGroup
})
} else {
return this.emojis.filter(pack => {
return pack.emojis.length > 0
})
}
},
stickerPickerEnabled () {
return (this.$store.state.instance.stickers || []).length !== 0
return (this.$store.state.instance.stickers || []).length !== 0 && this.enableStickerPicker
}
}
}

View File

@ -1,13 +1,32 @@
@import '../../_variables.scss';
.Notification {
.emoji-picker {
min-width: 160%;
width: 150%;
overflow: hidden;
left: -70%;
max-width: 100%;
@media (min-width: 800px) and (max-width: 1300px) {
left: -50%;
min-width: 50%;
max-width: 130%;
}
@media (max-width: 800px) {
left: -10%;
min-width: 50%;
max-width: 130%;
}
}
}
.emoji-picker {
display: flex;
flex-direction: column;
position: absolute;
right: 0;
left: 0;
margin: 0 !important;
z-index: 1;
z-index: 100;
background-color: $fallback--bg;
background-color: var(--popover, $fallback--bg);
color: $fallback--link;
@ -35,9 +54,8 @@
}
.heading {
display: flex;
height: 32px;
padding: 10px 7px 5px;
margin-top: 10px;
height: 4.8em;
}
.content {
@ -51,10 +69,6 @@
flex-grow: 1;
}
.emoji-groups {
min-height: 200px;
}
.additional-tabs {
border-left: 1px solid;
border-left-color: $fallback--icon;
@ -65,20 +79,40 @@
.additional-tabs,
.emoji-tabs {
position: absolute;
display: block;
min-width: 0;
flex-basis: auto;
flex-shrink: 1;
flex-wrap: nowrap;
overflow: auto;
width: 100%;
white-space: nowrap;
&-item {
padding: 0 7px;
vertical-align: top;
display: inline-flex;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
padding: .4em;
cursor: pointer;
font-size: 24px;
img {
max-width: 100%;
max-height: 100%;
object-fit: contain;
}
span {
font-size: 1.9em;
}
&.disabled {
opacity: 0.5;
pointer-events: none;
}
&.active {
border-bottom: 4px solid;
@ -87,6 +121,9 @@
color: var(--lightText, $fallback--lightText);
}
}
.fa-sticky-note {
font-size: 2em;
}
}
}
@ -110,75 +147,12 @@
}
}
.emoji {
&-search {
padding: 5px;
flex: 0 0 auto;
.emoji-search {
padding: 5px;
flex: 0 0 auto;
input {
width: 100%;
}
input {
width: 100%;
}
&-groups {
flex: 1 1 1px;
position: relative;
overflow: auto;
user-select: none;
mask: linear-gradient(to top, white 0, transparent 100%) bottom no-repeat,
linear-gradient(to bottom, white 0, transparent 100%) top no-repeat,
linear-gradient(to top, white, white);
transition: mask-size 150ms;
mask-size: 100% 20px, 100% 20px, auto;
// Autoprefixed seem to ignore this one, and also syntax is different
-webkit-mask-composite: xor;
mask-composite: exclude;
&.scrolled {
&-top {
mask-size: 100% 20px, 100% 0, auto;
}
&-bottom {
mask-size: 100% 0, 100% 20px, auto;
}
}
}
&-group {
display: flex;
align-items: center;
flex-wrap: wrap;
padding-left: 5px;
justify-content: left;
&-title {
font-size: 12px;
width: 100%;
margin: 0;
&.disabled {
display: none;
}
}
}
&-item {
width: 32px;
height: 32px;
box-sizing: border-box;
display: flex;
font-size: 32px;
align-items: center;
justify-content: center;
margin: 4px;
cursor: pointer;
img {
object-fit: contain;
max-width: 100%;
max-height: 100%;
}
}
}
}

View File

@ -1,7 +1,11 @@
<template>
<div class="emoji-picker panel panel-default panel-body">
<div class="heading">
<span class="emoji-tabs">
<span
ref="emoji-tabs"
class="emoji-tabs"
@wheel="onWheel"
>
<span
v-for="group in emojis"
:key="group.id"
@ -13,18 +17,15 @@
:title="group.text"
@click.prevent="highlight(group.id)"
>
<FAIcon
:icon="group.icon"
fixed-width
/>
<span v-if="!group.first.imageUrl">{{ group.first.replacement }}</span>
<img
v-else
:src="group.first.imageUrl"
>
</span>
</span>
<span
v-if="stickerPickerEnabled"
class="additional-tabs"
>
<span
class="stickers-tab-icon additional-tabs-item"
v-if="stickerPickerEnabled"
class="stickers-tab-icon emoji-tabs-item"
:class="{active: showingStickers}"
:title="$t('emoji.stickers')"
@click.prevent="toggleStickers"
@ -47,41 +48,15 @@
type="text"
class="form-control"
:placeholder="$t('emoji.search_emoji')"
@input="$event.target.composing = false"
>
</div>
<div
ref="emoji-groups"
class="emoji-groups"
:class="groupsScrolledClass"
@scroll="onScroll"
>
<div
v-for="group in emojisView"
:key="group.id"
class="emoji-group"
>
<h6
:ref="'group-' + group.id"
class="emoji-group-title"
>
{{ group.text }}
</h6>
<span
v-for="emoji in group.emojis"
:key="group.id + emoji.displayText"
:title="emoji.displayText"
class="emoji-item"
@click.stop.prevent="onEmoji(emoji)"
>
<span v-if="!emoji.imageUrl">{{ emoji.replacement }}</span>
<img
v-else
:src="emoji.imageUrl"
>
</span>
<span :ref="'group-end-' + group.id" />
</div>
</div>
<EmojiGrid
ref="emojiGrid"
:groups="emojisView"
@emoji="onEmoji"
@active-group="onActiveGroup"
/>
<div class="keep-open">
<Checkbox v-model="keepOpen">
{{ $t('emoji.keep_open') }}

View File

@ -27,7 +27,11 @@ const EmojiReactions = {
},
accountsForEmoji () {
return this.status.emoji_reactions.reduce((acc, reaction) => {
acc[reaction.name] = reaction.accounts || []
if (reaction.url) {
acc[reaction.url] = reaction.accounts || []
} else {
acc[reaction.name] = reaction.accounts || []
}
return acc
}, {})
},
@ -42,6 +46,14 @@ const EmojiReactions = {
reactedWith (emoji) {
return this.status.emoji_reactions.find(r => r.name === emoji).me
},
isLocalReaction (emojiUrl) {
if (!emojiUrl) return true
const reacted = this.accountsForEmoji[emojiUrl]
if (reacted.length === 0) {
return true
}
return reacted[0].is_local
},
fetchEmojiReactionsByIfMissing () {
const hasNoAccounts = this.status.emoji_reactions.find(r => !r.accounts)
if (hasNoAccounts) {

View File

@ -2,8 +2,8 @@
<div class="emoji-reactions">
<UserListPopover
v-for="(reaction) in emojiReactions"
:key="reaction.name"
:users="accountsForEmoji[reaction.name]"
:key="reaction.url || reaction.name"
:users="accountsForEmoji[reaction.url || reaction.name]"
>
<button
class="emoji-reaction btn button-default"
@ -11,8 +11,23 @@
@click="emojiOnClick(reaction.name, $event)"
@mouseenter="fetchEmojiReactionsByIfMissing()"
>
<span class="reaction-emoji">{{ reaction.name }}</span>
<span>{{ reaction.count }}</span>
<span
v-if="reaction.url !== null"
>
<img
:src="reaction.url"
:title="reaction.name"
class="emoji"
height="32px"
>
{{ reaction.count }}
</span>
<span v-else>
<span class="reaction-emoji unicode-emoji">
{{ reaction.name }}
</span>
<span>{{ reaction.count }}</span>
</span>
</button>
</UserListPopover>
<a
@ -36,6 +51,10 @@
flex-wrap: wrap;
}
.unicode-emoji {
font-size: 185%;
}
.emoji-reaction {
padding: 0 0.5em;
margin-right: 0.5em;
@ -44,10 +63,6 @@
align-items: center;
justify-content: center;
box-sizing: border-box;
.reaction-emoji {
width: 1.25em;
margin-right: 0.25em;
}
&:focus {
outline: none;
}
@ -73,7 +88,7 @@
}
}
.picked-reaction {
.button-default.picked-reaction {
border: 1px solid var(--accent, $fallback--link);
margin-left: -1px; // offset the border, can't use inset shadows either
margin-right: calc(0.5em - 1px);

View File

@ -1,6 +1,6 @@
import { requestFollow, requestUnfollow } from '../../services/follow_manipulate/follow_manipulate'
export default {
props: ['relationship', 'labelFollowing', 'buttonClass'],
props: ['relationship', 'user', 'labelFollowing', 'buttonClass'],
data () {
return {
inProgress: false
@ -14,7 +14,7 @@ export default {
if (this.inProgress || this.relationship.following) {
return this.$t('user_card.follow_unfollow')
} else if (this.relationship.requested) {
return this.$t('user_card.follow_again')
return this.$t('user_card.follow_cancel')
} else {
return this.$t('user_card.follow')
}
@ -29,11 +29,14 @@ export default {
} else {
return this.$t('user_card.follow')
}
},
disabled () {
return this.inProgress || this.user.deactivated
}
},
methods: {
onClick () {
this.relationship.following ? this.unfollow() : this.follow()
this.relationship.following || this.relationship.requested ? this.unfollow() : this.follow()
},
follow () {
this.inProgress = true

View File

@ -2,7 +2,7 @@
<button
class="btn button-default follow-button"
:class="{ toggled: isPressed }"
:disabled="inProgress"
:disabled="disabled"
:title="title"
@click="onClick"
>

View File

@ -20,6 +20,7 @@
:relationship="relationship"
:label-following="$t('user_card.follow_unfollow')"
class="follow-card-follow-button"
:user="user"
/>
</template>
</div>

View File

@ -1,17 +1,56 @@
import { mapState } from 'vuex'
import { get } from 'lodash'
/**
* This is for backwards compatibility. We originally didn't recieve
* extra info like a reason why an instance was rejected/quarantined/etc.
* Because we didn't want to break backwards compatibility it was decided
* to add an extra "info" key.
*/
const toInstanceReasonObject = (instances, info, key) => {
return instances.map(instance => {
if (info[key] && info[key][instance] && info[key][instance]['reason']) {
return { instance: instance, reason: info[key][instance]['reason'] }
}
return { instance: instance, reason: '' }
})
}
const MRFTransparencyPanel = {
computed: {
...mapState({
federationPolicy: state => get(state, 'instance.federationPolicy'),
mrfPolicies: state => get(state, 'instance.federationPolicy.mrf_policies', []),
quarantineInstances: state => get(state, 'instance.federationPolicy.quarantined_instances', []),
acceptInstances: state => get(state, 'instance.federationPolicy.mrf_simple.accept', []),
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', []),
quarantineInstances: state => toInstanceReasonObject(
get(state, 'instance.federationPolicy.quarantined_instances', []),
get(state, 'instance.federationPolicy.quarantined_instances_info', []),
'quarantined_instances'
),
acceptInstances: state => toInstanceReasonObject(
get(state, 'instance.federationPolicy.mrf_simple.accept', []),
get(state, 'instance.federationPolicy.mrf_simple_info', []),
'accept'
),
rejectInstances: state => toInstanceReasonObject(
get(state, 'instance.federationPolicy.mrf_simple.reject', []),
get(state, 'instance.federationPolicy.mrf_simple_info', []),
'reject'
),
ftlRemovalInstances: state => toInstanceReasonObject(
get(state, 'instance.federationPolicy.mrf_simple.federated_timeline_removal', []),
get(state, 'instance.federationPolicy.mrf_simple_info', []),
'federated_timeline_removal'
),
mediaNsfwInstances: state => toInstanceReasonObject(
get(state, 'instance.federationPolicy.mrf_simple.media_nsfw', []),
get(state, 'instance.federationPolicy.mrf_simple_info', []),
'media_nsfw'
),
mediaRemovalInstances: state => toInstanceReasonObject(
get(state, 'instance.federationPolicy.mrf_simple.media_removal', []),
get(state, 'instance.federationPolicy.mrf_simple_info', []),
'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', [])

View File

@ -0,0 +1,21 @@
.mrf-section {
margin: 1em;
table {
width:100%;
text-align: left;
padding-left:10px;
padding-bottom:20px;
th, td {
width: 180px;
max-width: 360px;
overflow: hidden;
vertical-align: text-top;
}
th+th, td+td {
width: auto;
}
}
}

View File

@ -31,13 +31,24 @@
<p>{{ $t("about.mrf.simple.accept_desc") }}</p>
<ul>
<li
v-for="instance in acceptInstances"
:key="instance"
v-text="instance"
/>
</ul>
<table>
<tr>
<th>{{ $t("about.mrf.simple.instance") }}</th>
<th>{{ $t("about.mrf.simple.reason") }}</th>
</tr>
<tr
v-for="entry in acceptInstances"
:key="entry.instance + '_accept'"
>
<td>{{ entry.instance }}</td>
<td v-if="entry.reason === ''">
{{ $t("about.mrf.simple.not_applicable") }}
</td>
<td v-else>
{{ entry.reason }}
</td>
</tr>
</table>
</div>
<div v-if="rejectInstances.length">
@ -45,13 +56,24 @@
<p>{{ $t("about.mrf.simple.reject_desc") }}</p>
<ul>
<li
v-for="instance in rejectInstances"
:key="instance"
v-text="instance"
/>
</ul>
<table>
<tr>
<th>{{ $t("about.mrf.simple.instance") }}</th>
<th>{{ $t("about.mrf.simple.reason") }}</th>
</tr>
<tr
v-for="entry in rejectInstances"
:key="entry.instance + '_reject'"
>
<td>{{ entry.instance }}</td>
<td v-if="entry.reason === ''">
{{ $t("about.mrf.simple.not_applicable") }}
</td>
<td v-else>
{{ entry.reason }}
</td>
</tr>
</table>
</div>
<div v-if="quarantineInstances.length">
@ -59,13 +81,24 @@
<p>{{ $t("about.mrf.simple.quarantine_desc") }}</p>
<ul>
<li
v-for="instance in quarantineInstances"
:key="instance"
v-text="instance"
/>
</ul>
<table>
<tr>
<th>{{ $t("about.mrf.simple.instance") }}</th>
<th>{{ $t("about.mrf.simple.reason") }}</th>
</tr>
<tr
v-for="entry in quarantineInstances"
:key="entry.instance + '_quarantine'"
>
<td>{{ entry.instance }}</td>
<td v-if="entry.reason === ''">
{{ $t("about.mrf.simple.not_applicable") }}
</td>
<td v-else>
{{ entry.reason }}
</td>
</tr>
</table>
</div>
<div v-if="ftlRemovalInstances.length">
@ -73,13 +106,24 @@
<p>{{ $t("about.mrf.simple.ftl_removal_desc") }}</p>
<ul>
<li
v-for="instance in ftlRemovalInstances"
:key="instance"
v-text="instance"
/>
</ul>
<table>
<tr>
<th>{{ $t("about.mrf.simple.instance") }}</th>
<th>{{ $t("about.mrf.simple.reason") }}</th>
</tr>
<tr
v-for="entry in ftlRemovalInstances"
:key="entry.instance + '_ftl_removal'"
>
<td>{{ entry.instance }}</td>
<td v-if="entry.reason === ''">
{{ $t("about.mrf.simple.not_applicable") }}
</td>
<td v-else>
{{ entry.reason }}
</td>
</tr>
</table>
</div>
<div v-if="mediaNsfwInstances.length">
@ -87,13 +131,24 @@
<p>{{ $t("about.mrf.simple.media_nsfw_desc") }}</p>
<ul>
<li
v-for="instance in mediaNsfwInstances"
:key="instance"
v-text="instance"
/>
</ul>
<table>
<tr>
<th>{{ $t("about.mrf.simple.instance") }}</th>
<th>{{ $t("about.mrf.simple.reason") }}</th>
</tr>
<tr
v-for="entry in mediaNsfwInstances"
:key="entry.instance + '_media_nsfw'"
>
<td>{{ entry.instance }}</td>
<td v-if="entry.reason === ''">
{{ $t("about.mrf.simple.not_applicable") }}
</td>
<td v-else>
{{ entry.reason }}
</td>
</tr>
</table>
</div>
<div v-if="mediaRemovalInstances.length">
@ -101,13 +156,24 @@
<p>{{ $t("about.mrf.simple.media_removal_desc") }}</p>
<ul>
<li
v-for="instance in mediaRemovalInstances"
:key="instance"
v-text="instance"
/>
</ul>
<table>
<tr>
<th>{{ $t("about.mrf.simple.instance") }}</th>
<th>{{ $t("about.mrf.simple.reason") }}</th>
</tr>
<tr
v-for="entry in mediaRemovalInstances"
:key="entry.instance + '_media_removal'"
>
<td>{{ entry.instance }}</td>
<td v-if="entry.reason === ''">
{{ $t("about.mrf.simple.not_applicable") }}
</td>
<td v-else>
{{ entry.reason }}
</td>
</tr>
</table>
</div>
<h2 v-if="hasKeywordPolicies">
@ -161,7 +227,6 @@
<script src="./mrf_transparency_panel.js"></script>
<style lang="scss">
.mrf-section {
margin: 1em;
}
@import '../../_variables.scss';
@import './mrf_transparency_panel.scss';
</style>

View File

@ -1,4 +1,4 @@
import TimelineMenuContent from '../timeline_menu/timeline_menu_content.vue'
import { timelineNames } from '../timeline_menu/timeline_menu.js'
import { mapState, mapGetters } from 'vuex'
import { library } from '@fortawesome/fontawesome-svg-core'
@ -7,12 +7,10 @@ import {
faGlobe,
faBookmark,
faEnvelope,
faChevronDown,
faChevronUp,
faHome,
faComments,
faBell,
faInfoCircle,
faStream
faInfoCircle
} from '@fortawesome/free-solid-svg-icons'
library.add(
@ -20,12 +18,10 @@ library.add(
faGlobe,
faBookmark,
faEnvelope,
faChevronDown,
faChevronUp,
faHome,
faComments,
faBell,
faInfoCircle,
faStream
faInfoCircle
)
const NavPanel = {
@ -34,20 +30,16 @@ const NavPanel = {
this.$store.dispatch('startFetchingFollowRequests')
}
},
components: {
TimelineMenuContent
},
data () {
return {
showTimelines: false
}
},
methods: {
toggleTimelines () {
this.showTimelines = !this.showTimelines
}
},
computed: {
onTimelineRoute () {
return !!timelineNames()[this.$route.name]
},
timelinesRoute () {
if (this.$store.state.interface.lastTimeline) {
return this.$store.state.interface.lastTimeline
}
return this.currentUser ? 'friends' : 'public-timeline'
},
...mapState({
currentUser: state => state.users.currentUser,
followRequestCount: state => state.api.followRequests.length,

View File

@ -3,33 +3,19 @@
<div class="panel panel-default">
<ul>
<li v-if="currentUser || !privateMode">
<button
class="button-unstyled menu-item"
@click="toggleTimelines"
<router-link
:to="{ name: timelinesRoute }"
:class="onTimelineRoute && 'router-link-active'"
>
<FAIcon
fixed-width
class="fa-scale-110"
icon="stream"
icon="home"
/>{{ $t("nav.timelines") }}
<FAIcon
class="timelines-chevron"
fixed-width
:icon="showTimelines ? 'chevron-up' : 'chevron-down'"
/>
</button>
<div
v-show="showTimelines"
class="timelines-background"
>
<TimelineMenuContent class="timelines" />
</div>
</router-link>
</li>
<li v-if="currentUser">
<router-link
class="menu-item"
:to="{ name: 'interactions', params: { username: currentUser.screen_name } }"
>
<router-link :to="{ name: 'interactions', params: { username: currentUser.screen_name } }">
<FAIcon
fixed-width
class="fa-scale-110"
@ -38,10 +24,7 @@
</router-link>
</li>
<li v-if="currentUser && pleromaChatMessagesAvailable">
<router-link
class="menu-item"
:to="{ name: 'chats', params: { username: currentUser.screen_name } }"
>
<router-link :to="{ name: 'chats', params: { username: currentUser.screen_name } }">
<div
v-if="unreadChatCount"
class="badge badge-notification"
@ -56,10 +39,7 @@
</router-link>
</li>
<li v-if="currentUser && currentUser.locked">
<router-link
class="menu-item"
:to="{ name: 'friend-requests' }"
>
<router-link :to="{ name: 'friend-requests' }">
<FAIcon
fixed-width
class="fa-scale-110"
@ -74,10 +54,7 @@
</router-link>
</li>
<li>
<router-link
class="menu-item"
:to="{ name: 'about' }"
>
<router-link :to="{ name: 'about' }">
<FAIcon
fixed-width
class="fa-scale-110"
@ -114,14 +91,14 @@
border-color: var(--border, $fallback--border);
padding: 0;
&:first-child .menu-item {
&:first-child a {
border-top-right-radius: $fallback--panelRadius;
border-top-right-radius: var(--panelRadius, $fallback--panelRadius);
border-top-left-radius: $fallback--panelRadius;
border-top-left-radius: var(--panelRadius, $fallback--panelRadius);
}
&:last-child .menu-item {
&:last-child a {
border-bottom-right-radius: $fallback--panelRadius;
border-bottom-right-radius: var(--panelRadius, $fallback--panelRadius);
border-bottom-left-radius: $fallback--panelRadius;
@ -133,15 +110,13 @@
border: none;
}
.menu-item {
a {
display: block;
box-sizing: border-box;
align-items: stretch;
height: 3.5em;
line-height: 3.5em;
padding: 0 1em;
width: 100%;
color: $fallback--link;
color: var(--link, $fallback--link);
&:hover {
background-color: $fallback--lightBg;
@ -171,25 +146,6 @@
}
}
.timelines-chevron {
margin-left: 0.8em;
font-size: 1.1em;
}
.timelines-background {
padding: 0 0 0 0.6em;
background-color: $fallback--lightBg;
background-color: var(--selectedMenu, $fallback--lightBg);
border-top: 1px solid;
border-color: $fallback--border;
border-color: var(--border, $fallback--border);
}
.timelines {
background-color: $fallback--bg;
background-color: var(--bg, $fallback--bg);
}
.fa-scale-110 {
margin-right: 0.8em;
}

View File

@ -1,5 +1,12 @@
@import '../../_variables.scss';
.notification-reaction-emoji {
width: 40px;
display: inline-flex;
vertical-align: middle;
flex-direction: column;
}
// TODO Copypaste from Status, should unify it somehow
.Notification {
&.-muted {

View File

@ -102,7 +102,18 @@
<span v-if="notification.type === 'pleroma:emoji_reaction'">
<small>
<i18n path="notifications.reacted_with">
<span class="emoji-reaction-emoji">{{ notification.emoji }}</span>
<img
v-if="notification.emoji_url !== null"
class="notification-reaction-emoji"
:src="notification.emoji_url"
:name="notification.emoji"
>
<span
v-else
class="emoji-reaction-emoji"
>
{{ notification.emoji }}
</span>
</i18n>
</small>
</span>

View File

@ -1,4 +1,5 @@
import Popover from '../popover/popover.vue'
import EmojiPicker from '../emoji_picker/emoji_picker.vue'
import { library } from '@fortawesome/fontawesome-svg-core'
import { faSmileBeam } from '@fortawesome/free-regular-svg-icons'
@ -12,17 +13,19 @@ const ReactButton = {
}
},
components: {
Popover
Popover,
EmojiPicker
},
methods: {
addReaction (event, emoji, close) {
addReaction (event, close) {
const emoji = event.insertion
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 })
}
close()
if (!event.keepOpen) { close() }
},
focusInput () {
this.$nextTick(() => {
@ -32,34 +35,6 @@ const ReactButton = {
}
},
computed: {
commonEmojis () {
return [
{ displayText: 'thumbsup', replacement: '👍' },
{ displayText: 'angry', replacement: '😠' },
{ displayText: 'eyes', replacement: '👀' },
{ displayText: 'joy', replacement: '😂' },
{ displayText: 'fire', replacement: '🔥' }
]
},
emojis () {
if (this.filterWord !== '') {
const filterWordLowercase = this.filterWord.toLowerCase()
let orderedEmojiList = []
for (const emoji of this.$store.state.instance.emoji) {
if (emoji.replacement === this.filterWord) return [emoji]
const indexOfFilterWord = emoji.displayText.toLowerCase().indexOf(filterWordLowercase)
if (indexOfFilterWord > -1) {
if (!Array.isArray(orderedEmojiList[indexOfFilterWord])) {
orderedEmojiList[indexOfFilterWord] = []
}
orderedEmojiList[indexOfFilterWord].push(emoji)
}
}
return orderedEmojiList.flat()
}
return this.$store.state.instance.emoji || []
},
mergedConfig () {
return this.$store.getters.mergedConfig
}

View File

@ -9,35 +9,10 @@
@show="focusInput"
>
<template v-slot:content="{close}">
<div class="reaction-picker-filter">
<input
v-model="filterWord"
size="1"
:placeholder="$t('emoji.search_emoji')"
>
</div>
<div class="reaction-picker">
<span
v-for="emoji in commonEmojis"
:key="emoji.replacement"
class="emoji-button"
:title="emoji.displayText"
@click="addReaction($event, emoji.replacement, close)"
>
{{ emoji.replacement }}
</span>
<div class="reaction-picker-divider" />
<span
v-for="(emoji, key) in emojis"
:key="key"
class="emoji-button"
:title="emoji.displayText"
@click="addReaction($event, emoji.replacement, close)"
>
{{ emoji.replacement }}
</span>
<div class="reaction-bottom-fader" />
</div>
<EmojiPicker
:enableStickerPicker="false"
@emoji="addReaction($event, close)"
/>
</template>
<template v-slot:trigger>
<button
@ -58,6 +33,10 @@
<style lang="scss">
@import '../../_variables.scss';
.custom-reaction {
width: 30px !important;
}
.ReactButton {
.reaction-picker-filter {
padding: 0.5em;

View File

@ -7,6 +7,7 @@ import {
faCircleNotch,
faSearch
} from '@fortawesome/free-solid-svg-icons'
import { uniqBy } from 'lodash'
library.add(
faCircleNotch,
@ -30,7 +31,10 @@ const Search = {
userIds: [],
statuses: [],
hashtags: [],
currenResultTab: 'statuses'
currenResultTab: 'statuses',
statusesOffset: 0,
lastStatusFetchCount: 0,
lastQuery: ''
}
},
computed: {
@ -59,7 +63,7 @@ const Search = {
this.$router.push({ name: 'search', query: { query } })
this.$refs.searchInput.focus()
},
search (query) {
search (query, searchType = null) {
if (!query) {
this.loading = false
return
@ -70,15 +74,34 @@ const Search = {
this.statuses = []
this.hashtags = []
this.$refs.searchInput.blur()
if (this.lastQuery !== query) {
this.userIds = []
this.hashtags = []
this.statuses = []
this.$store.dispatch('search', { q: query, resolve: true })
this.statusesOffset = 0
this.lastStatusFetchCount = 0
}
this.$store.dispatch('search', { q: query, resolve: true, offset: this.statusesOffset, 'type': searchType })
.then(data => {
this.loading = false
this.userIds = map(data.accounts, 'id')
this.statuses = data.statuses
this.hashtags = data.hashtags
let oldLength = this.statuses.length
// Always append to old results. If new results are empty, this doesn't change anything
this.userIds = this.userIds.concat(map(data.accounts, 'id'))
this.statuses = uniqBy(this.statuses.concat(data.statuses), 'id')
this.hashtags = this.hashtags.concat(data.hashtags)
this.currenResultTab = this.getActiveTab()
this.loaded = true
// Offset from whatever we already have
this.statusesOffset = this.statuses.length
// Because the amount of new statuses can actually be zero, compare to old lenght instead
this.lastStatusFetchCount = this.statuses.length - oldLength
this.lastQuery = query
})
},
resultCount (tabName) {

View File

@ -22,7 +22,7 @@
</button>
</div>
<div
v-if="loading"
v-if="loading && statusesOffset == 0"
class="text-center loading-icon"
>
<FAIcon
@ -54,13 +54,7 @@
</div>
</div>
<div class="panel-body">
<div v-if="currenResultTab === 'statuses'">
<div
v-if="visibleStatuses.length === 0 && !loading && loaded"
class="search-result-heading"
>
<h4>{{ $t('search.no_results') }}</h4>
</div>
<div v-if="currenResultTab === 'statuses'">
<Status
v-for="status in visibleStatuses"
:key="status.id"
@ -71,6 +65,33 @@
:statusoid="status"
:no-heading="false"
/>
<button
v-if="!loading && loaded && lastStatusFetchCount > 0"
class="more-statuses-button button-unstyled -link -fullwidth"
@click.prevent="search(searchTerm, 'statuses')"
>
<div class="new-status-notification text-center">
{{ $t('search.load_more') }}
</div>
</button>
<div
v-else-if="loading && statusesOffset > 0"
class="text-center loading-icon"
>
<FAIcon
icon="circle-notch"
spin
size="lg"
/>
</div>
<div
v-if="(visibleStatuses.length === 0 || lastStatusFetchCount === 0) && !loading && loaded"
class="search-result-heading"
>
<h4>
{{ visibleStatuses.length === 0 ? $t('search.no_results') : $t('search.no_more_results') }}
</h4>
</div>
</div>
<div v-else-if="currenResultTab === 'people'">
<div
@ -208,6 +229,11 @@
color: $fallback--text;
color: var(--text, $fallback--text);
}
.more-statuses-button {
height: 3.5em;
line-height: 3.5em;
}
}
</style>

View File

@ -16,6 +16,11 @@
{{ $t('settings.right_sidebar') }}
</BooleanSetting>
</li>
<li>
<BooleanSetting path="showThirdColumn">
{{ $t('settings.show_third_column') }}
</BooleanSetting>
</li>
<li v-if="instanceWallpaperUsed">
<BooleanSetting path="hideInstanceWallpaper">
{{ $t('settings.hide_wallpaper') }}

View File

@ -1,17 +1,29 @@
import Popover from '../popover/popover.vue'
import TimelineMenuContent from './timeline_menu_content.vue'
import { mapState } from 'vuex'
import { library } from '@fortawesome/fontawesome-svg-core'
import {
faUsers,
faGlobe,
faBookmark,
faEnvelope,
faHome,
faChevronDown
} from '@fortawesome/free-solid-svg-icons'
library.add(faChevronDown)
library.add(
faUsers,
faGlobe,
faBookmark,
faEnvelope,
faHome,
faChevronDown
)
// Route -> i18n key mapping, exported and not in the computed
// because nav panel benefits from the same information.
export const timelineNames = () => {
return {
'friends': 'nav.home_timeline',
'friends': 'nav.timeline',
'bookmarks': 'nav.bookmarks',
'dms': 'nav.dms',
'public-timeline': 'nav.public_tl',
@ -21,8 +33,7 @@ export const timelineNames = () => {
const TimelineMenu = {
components: {
Popover,
TimelineMenuContent
Popover
},
data () {
return {
@ -30,6 +41,9 @@ const TimelineMenu = {
}
},
created () {
if (this.currentUser && this.currentUser.locked) {
this.$store.dispatch('startFetchingFollowRequests')
}
if (timelineNames()[this.$route.name]) {
this.$store.dispatch('setLastTimeline', this.$route.name)
}
@ -61,6 +75,13 @@ const TimelineMenu = {
const i18nkey = timelineNames()[this.$route.name]
return i18nkey ? this.$t(i18nkey) : route
}
},
computed: {
...mapState({
currentUser: state => state.users.currentUser,
privateMode: state => state.instance.private,
federating: state => state.instance.federating
})
}
}

View File

@ -9,26 +9,74 @@
@show="openMenu"
@close="() => isOpen = false"
>
<template v-slot:content>
<div class="timeline-menu-popover popover-default">
<TimelineMenuContent />
</div>
</template>
<template v-slot:trigger>
<button class="button-unstyled title timeline-menu-title">
<span class="timeline-title">{{ timelineName() }}</span>
<span>
<FAIcon
size="sm"
icon="chevron-down"
/>
</span>
<span
class="click-blocker"
@click="blockOpen"
<div
slot="content"
class="timeline-menu-popover panel panel-default"
>
<ul>
<li v-if="currentUser">
<router-link :to="{ name: 'friends' }">
<FAIcon
fixed-width
class="fa-scale-110 fa-old-padding "
icon="home"
/>{{ $t("nav.timeline") }}
</router-link>
</li>
<li v-if="currentUser">
<router-link :to="{ name: 'bookmarks'}">
<FAIcon
fixed-width
class="fa-scale-110 fa-old-padding "
icon="bookmark"
/>{{ $t("nav.bookmarks") }}
</router-link>
</li>
<li v-if="currentUser">
<router-link :to="{ name: 'dms', params: { username: currentUser.screen_name } }">
<FAIcon
fixed-width
class="fa-scale-110 fa-old-padding "
icon="envelope"
/>{{ $t("nav.dms") }}
</router-link>
</li>
<li v-if="currentUser || !privateMode">
<router-link :to="{ name: 'public-timeline' }">
<FAIcon
fixed-width
class="fa-scale-110 fa-old-padding "
icon="users"
/>{{ $t("nav.public_tl") }}
</router-link>
</li>
<li v-if="federating && (currentUser || !privateMode)">
<router-link :to="{ name: 'public-external-timeline' }">
<FAIcon
fixed-width
class="fa-scale-110 fa-old-padding "
icon="globe"
/>{{ $t("nav.twkn") }}
</router-link>
</li>
</ul>
</div>
<div
slot="trigger"
class="title timeline-menu-title"
>
<span class="timeline-title">{{ timelineName() }}</span>
<span>
<FAIcon
size="sm"
icon="chevron-down"
/>
</button>
</template>
</span>
<span
class="click-blocker"
@click="blockOpen"
/>
</div>
</Popover>
</template>

View File

@ -1,29 +0,0 @@
import { mapState } from 'vuex'
import { library } from '@fortawesome/fontawesome-svg-core'
import {
faUsers,
faGlobe,
faBookmark,
faEnvelope,
faHome
} from '@fortawesome/free-solid-svg-icons'
library.add(
faUsers,
faGlobe,
faBookmark,
faEnvelope,
faHome
)
const TimelineMenuContent = {
computed: {
...mapState({
currentUser: state => state.users.currentUser,
privateMode: state => state.instance.private,
federating: state => state.instance.federating
})
}
}
export default TimelineMenuContent

View File

@ -1,66 +0,0 @@
<template>
<ul>
<li v-if="currentUser">
<router-link
class="menu-item"
:to="{ name: 'friends' }"
>
<FAIcon
fixed-width
class="fa-scale-110 fa-old-padding "
icon="home"
/>{{ $t("nav.home_timeline") }}
</router-link>
</li>
<li v-if="currentUser || !privateMode">
<router-link
class="menu-item"
:to="{ name: 'public-timeline' }"
>
<FAIcon
fixed-width
class="fa-scale-110 fa-old-padding "
icon="users"
/>{{ $t("nav.public_tl") }}
</router-link>
</li>
<li v-if="federating && (currentUser || !privateMode)">
<router-link
class="menu-item"
:to="{ name: 'public-external-timeline' }"
>
<FAIcon
fixed-width
class="fa-scale-110 fa-old-padding "
icon="globe"
/>{{ $t("nav.twkn") }}
</router-link>
</li>
<li v-if="currentUser">
<router-link
class="menu-item"
:to="{ name: 'bookmarks'}"
>
<FAIcon
fixed-width
class="fa-scale-110 fa-old-padding "
icon="bookmark"
/>{{ $t("nav.bookmarks") }}
</router-link>
</li>
<li v-if="currentUser">
<router-link
class="menu-item"
:to="{ name: 'dms', params: { username: currentUser.screen_name } }"
>
<FAIcon
fixed-width
class="fa-scale-110 fa-old-padding "
icon="envelope"
/>{{ $t("nav.dms") }}
</router-link>
</li>
</ul>
</template>
<script src="./timeline_menu_content.js" ></script>

View File

@ -65,7 +65,7 @@
:title="$t('user_card.edit_profile')"
/>
</button>
<button
<a
v-if="isOtherUser && !user.is_local"
:href="user.statusnet_profile_url"
target="_blank"
@ -75,7 +75,7 @@
class="icon"
icon="external-link-alt"
/>
</button>
</a>
<AccountActions
v-if="isOtherUser && loggedIn"
:user="user"
@ -91,6 +91,12 @@
@{{ user.screen_name_ui }}
</router-link>
<template v-if="!hideBio">
<span
v-if="user.deactivated"
class="alert user-role"
>
{{ $t('user_card.deactivated') }}
</span>
<span
v-if="!!visibleRole"
class="alert user-role"
@ -119,6 +125,12 @@
</div>
</div>
<div class="user-meta">
<div
v-if="relationship.blocked_by && loggedIn && isOtherUser"
class="blocking"
>
{{ $t('user_card.blocks_you') }}
</div>
<div
v-if="relationship.followed_by && loggedIn && isOtherUser"
class="following"
@ -169,7 +181,10 @@
class="user-interactions"
>
<div class="btn-group">
<FollowButton :relationship="relationship" />
<FollowButton
:relationship="relationship"
:user="user"
/>
<template v-if="relationship.following">
<ProgressButton
v-if="!relationship.subscribing"
@ -204,6 +219,7 @@
<button
v-if="relationship.muting"
class="btn button-default btn-block toggled"
:disabled="user.deactivated"
@click="unmuteUser"
>
{{ $t('user_card.muted') }}
@ -211,6 +227,7 @@
<button
v-else
class="btn button-default btn-block"
:disabled="user.deactivated"
@click="muteUser"
>
{{ $t('user_card.mute') }}
@ -219,6 +236,7 @@
<div>
<button
class="btn button-default btn-block"
:disabled="user.deactivated"
@click="mentionUser"
>
{{ $t('user_card.mention') }}
@ -533,7 +551,7 @@
line-height: 22px;
flex-wrap: wrap;
.following {
.following, .blocking {
flex: 1 0 auto;
margin: 0;
margin-bottom: .25em;

View File

@ -407,7 +407,6 @@
"follow": "Sledovat",
"follow_sent": "Požadavek odeslán!",
"follow_progress": "Odeslílám požadavek…",
"follow_again": "Odeslat požadavek znovu?",
"follow_unfollow": "Přestat sledovat",
"followees": "Sledovaní",
"followers": "Sledující",

View File

@ -564,7 +564,6 @@
"follow": "Folgen",
"follow_sent": "Anfrage gesendet!",
"follow_progress": "Anfragen…",
"follow_again": "Anfrage erneut senden?",
"follow_unfollow": "Folgen beenden",
"followees": "Folgt",
"followers": "Folgende",

View File

@ -13,6 +13,9 @@
"mrf_policies_desc": "MRF policies manipulate the federation behaviour of the instance. The following policies are enabled:",
"simple": {
"simple_policies": "Instance-specific policies",
"instance": "Instance",
"reason": "Reason",
"not_applicable": "N/A",
"accept": "Accept",
"accept_desc": "This instance only accepts messages from the following instances:",
"reject": "Reject",
@ -30,7 +33,7 @@
"staff": "Staff"
},
"shoutbox": {
"title": "Shoutbox"
"title": "Brapbox"
},
"domain_mute_card": {
"mute": "Mute",
@ -139,7 +142,7 @@
},
"notifications": {
"broken_favorite": "Unknown status, searching for it…",
"error": "Error fetching notifications: {0}",
"error": "Error fetching notifications",
"favorited_you": "favorited your status",
"followed_you": "followed you",
"follow_request": "wants to follow you",
@ -200,7 +203,7 @@
"text/bbcode": "BBCode"
},
"content_warning": "Subject (optional)",
"default": "Just landed in L.A.",
"default": "Dahling",
"direct_warning_to_all": "This post will be visible to all the mentioned users.",
"direct_warning_to_first_only": "This post will only be visible to the mentioned users at the beginning of the message.",
"posting": "Posting",
@ -449,6 +452,7 @@
"reset_banner_confirm": "Do you really want to reset the banner?",
"reset_background_confirm": "Do you really want to reset the background?",
"settings": "Settings",
"show_third_column": "Show separate notifications column",
"subject_input_always_show": "Always show subject field",
"subject_line_behavior": "Copy subject when replying",
"subject_line_email": "Like email: \"re: subject\"",
@ -624,7 +628,7 @@
"button": "Button",
"text": "A bunch of more {0} and {1}",
"mono": "content",
"input": "Just landed in L.A.",
"input": "Dahling",
"faint_link": "helpful manual",
"fine_print": "Read our {0} to learn nothing useful!",
"header_faint": "This is fine",
@ -675,7 +679,7 @@
"timeline": {
"collapse": "Collapse",
"conversation": "Conversation",
"error": "Error fetching timeline: {0}",
"error": "Error loading status",
"load_older": "Load older statuses",
"no_retweet_hint": "Post is marked as followers-only or direct and cannot be repeated",
"repeated": "repeated",
@ -718,13 +722,15 @@
"approve": "Approve",
"block": "Block",
"blocked": "Blocked!",
"blocks_you": "Blocks you!",
"deactivated": "Deactivated",
"deny": "Deny",
"edit_profile": "Edit profile",
"favorites": "Favorites",
"follow": "Follow",
"follow_cancel": "Cancel request",
"follow_sent": "Request sent!",
"follow_progress": "Requesting…",
"follow_again": "Send request again?",
"follow_unfollow": "Unfollow",
"followees": "Following",
"followers": "Followers",
@ -827,7 +833,9 @@
"hashtags": "Hashtags",
"person_talking": "{count} person talking",
"people_talking": "{count} people talking",
"no_results": "No results"
"no_results": "No results",
"no_more_results": "No more results",
"load_more": "Load more results"
},
"password_reset": {
"forgot_password": "Forgot password?",

View File

@ -557,7 +557,6 @@
"follow": "Aboni",
"follow_sent": "Peto sendiĝis!",
"follow_progress": "Petante…",
"follow_again": "Ĉu sendi peton ree?",
"follow_unfollow": "Malaboni",
"followees": "Abonatoj",
"followers": "Abonantoj",

View File

@ -679,7 +679,6 @@
"follow": "Seguir",
"follow_sent": "¡Solicitud enviada!",
"follow_progress": "Solicitando…",
"follow_again": "¿Enviar solicitud de nuevo?",
"follow_unfollow": "Dejar de seguir",
"followees": "Siguiendo",
"followers": "Seguidores",

View File

@ -550,7 +550,6 @@
"follow": "Jarraitu",
"follow_sent": "Eskaera bidalita!",
"follow_progress": "Eskatzen…",
"follow_again": "Eskaera berriro bidali?",
"follow_unfollow": "Jarraitzeari utzi",
"followees": "Jarraitzen",
"followers": "Jarraitzaileak",

View File

@ -589,7 +589,6 @@
"follow": "Seuraa",
"follow_sent": "Pyyntö lähetetty!",
"follow_progress": "Pyydetään…",
"follow_again": "Lähetä pyyntö uudestaan?",
"follow_unfollow": "Älä seuraa",
"followees": "Seuraa",
"followers": "Seuraajat",

View File

@ -607,7 +607,6 @@
"follow": "Suivre",
"follow_sent": "Demande envoyée !",
"follow_progress": "Demande en cours…",
"follow_again": "Renvoyer la demande ?",
"follow_unfollow": "Désabonner",
"followees": "Suivis",
"followers": "Vous suivent",

View File

@ -312,7 +312,6 @@
"follow": "עקוב",
"follow_sent": "בקשה נשלחה!",
"follow_progress": "מבקש…",
"follow_again": "שלח בקשה שוב?",
"follow_unfollow": "בטל עקיבה",
"followees": "נעקבים",
"followers": "עוקבים",

View File

@ -511,7 +511,6 @@
"its_you": "Sei tu!",
"hidden": "Nascosto",
"follow_unfollow": "Disconosci",
"follow_again": "Reinvio richiesta?",
"follow_progress": "Richiedo…",
"follow_sent": "Richiesta inviata!",
"favorites": "Preferiti",

View File

@ -567,7 +567,6 @@
"follow": "フォロー",
"follow_sent": "リクエストを、おくりました!",
"follow_progress": "リクエストしています…",
"follow_again": "ふたたびリクエストをおくりますか?",
"follow_unfollow": "フォローをやめる",
"followees": "フォロー",
"followers": "フォロワー",

View File

@ -679,7 +679,6 @@
"follow": "フォロー",
"follow_sent": "リクエストを送りました!",
"follow_progress": "リクエストしています…",
"follow_again": "再びリクエストを送りますか?",
"follow_unfollow": "フォローをやめる",
"followees": "フォロー",
"followers": "フォロワー",

View File

@ -428,7 +428,6 @@
"follow": "팔로우",
"follow_sent": "요청 보내짐!",
"follow_progress": "요청 중…",
"follow_again": "요청을 다시 보낼까요?",
"follow_unfollow": "팔로우 중지",
"followees": "팔로우 중",
"followers": "팔로워",

View File

@ -516,7 +516,6 @@
"follow": "Følg",
"follow_sent": "Forespørsel sendt!",
"follow_progress": "Forespør…",
"follow_again": "Gjenta forespørsel?",
"follow_unfollow": "Avfølg",
"followees": "Følger",
"followers": "Følgere",

View File

@ -565,9 +565,9 @@
"deny": "Weigeren",
"favorites": "Favorieten",
"follow": "Volgen",
"follow_cancel": "Aanvraag annuleren",
"follow_sent": "Aanvraag verzonden!",
"follow_progress": "Aanvragen…",
"follow_again": "Aanvraag opnieuw zenden?",
"follow_unfollow": "Stop volgen",
"followees": "Aan het volgen",
"followers": "Volgers",
@ -670,6 +670,9 @@
"mrf_policies": "Ingeschakelde MRF-regels",
"simple": {
"simple_policies": "Instantiespecifieke regels",
"instance": "Instantie",
"reason": "Reden",
"not_applicable": "n.v.t.",
"accept": "Accepteren",
"accept_desc": "Deze instantie accepteert alleen berichten van de volgende instanties:",
"reject": "Afwijzen",

View File

@ -465,7 +465,6 @@
"follow": "Seguir",
"follow_sent": "Demanda enviada!",
"follow_progress": "Demanda…",
"follow_again": "Tornar enviar la demanda?",
"follow_unfollow": "Quitar de seguir",
"followees": "Abonaments",
"followers": "Seguidors",

View File

@ -686,7 +686,6 @@
"follow": "Obserwuj",
"follow_sent": "Wysłano prośbę!",
"follow_progress": "Wysyłam prośbę…",
"follow_again": "Wysłać prośbę ponownie?",
"follow_unfollow": "Przestań obserwować",
"followees": "Obserwowani",
"followers": "Obserwujący",

View File

@ -575,7 +575,6 @@
"follow": "Seguir",
"follow_sent": "Pedido enviado!",
"follow_progress": "Enviando…",
"follow_again": "Enviar solicitação novamente?",
"follow_unfollow": "Deixar de seguir",
"followees": "Seguindo",
"followers": "Seguidores",

View File

@ -550,7 +550,6 @@
"follow": "Читать",
"follow_sent": "Запрос отправлен!",
"follow_progress": "Запрашиваем…",
"follow_again": "Запросить еще раз?",
"follow_unfollow": "Перестать читать",
"followees": "Читаемые",
"followers": "Читатели",

View File

@ -281,7 +281,7 @@
"settings.style.preview.button": "Button",
"settings.style.preview.text": "A bunch of more {0} and {1}",
"settings.style.preview.mono": "content",
"settings.style.preview.input": "Just landed in L.A.",
"settings.style.preview.input": "Dahling",
"settings.style.preview.faint_link": "helpful manual",
"settings.style.preview.fine_print": "Read our {0} to learn nothing useful!",
"settings.style.preview.header_faint": "This is fine",
@ -310,7 +310,6 @@
"user_card.follow": "Follow",
"user_card.follow_sent": "Request sent!",
"user_card.follow_progress": "Requesting…",
"user_card.follow_again": "Send request again?",
"user_card.follow_unfollow": "Unfollow",
"user_card.followees": "Following",
"user_card.followers": "Followers",

View File

@ -743,7 +743,6 @@
"message": "Повідомлення",
"follow": "Підписатись",
"follow_unfollow": "Відписатись",
"follow_again": "Відправити запит знову?",
"follow_sent": "Запит відправлено!",
"blocked": "Заблоковано!",
"admin_menu": {

View File

@ -672,7 +672,6 @@
"follow": "关注",
"follow_sent": "请求已发送!",
"follow_progress": "请求中…",
"follow_again": "再次发送请求?",
"follow_unfollow": "取消关注",
"followees": "正在关注",
"followers": "关注者",

View File

@ -766,7 +766,6 @@
"follow": "關注",
"follow_sent": "請求已發送!",
"follow_progress": "請求中…",
"follow_again": "再次發送請求?",
"follow_unfollow": "取消關注",
"followees": "正在關注",
"followers": "關注者",

View File

@ -66,6 +66,20 @@ const persistedStateOptions = {
};
(async () => {
if ('serviceWorker' in navigator) {
// declaring scope manually
navigator.serviceWorker.register('/sw-pleroma.js', { scope: '/' }).then(
(registration) => {
console.log('Service worker registration succeeded:', registration)
},
/* catch */ (error) => {
console.error(`Service worker registration failed: ${error}`)
}
)
} else {
console.error('Service workers are not supported.')
}
let storageError = false
const plugins = [pushNotifications]
try {

View File

@ -56,6 +56,7 @@ export const defaultState = {
hideScopeNotice: false,
useStreamingApi: false,
sidebarRight: undefined, // instance default
showThirdColumn: false, // instance default
scopeCopy: undefined, // instance default
subjectLineBehavior: undefined, // instance default
alwaysShowSubjectInput: undefined, // instance default

View File

@ -140,7 +140,7 @@ const instance = {
async getCustomEmoji ({ commit, state }) {
try {
const res = await window.fetch('/api/pleroma/emoji.json')
const res = await window.fetch('/api/v1/pleroma/emoji')
if (res.ok) {
const result = await res.json()
const values = Array.isArray(result) ? Object.assign({}, ...result) : result

View File

@ -747,8 +747,8 @@ const statuses = {
rootState.api.backendInteractor.fetchRebloggedByUsers({ id })
.then(rebloggedByUsers => commit('addRepeats', { id, rebloggedByUsers, currentUser: rootState.users.currentUser }))
},
search (store, { q, resolve, limit, offset, following }) {
return store.rootState.api.backendInteractor.search2({ q, resolve, limit, offset, following })
search (store, { q, resolve, limit, offset, following, type }) {
return store.rootState.api.backendInteractor.search2({ q, resolve, limit, offset, following, type })
.then((data) => {
store.commit('addNewUsers', data.accounts)
store.commit('addNewStatuses', { statuses: data.statuses })

View File

@ -388,7 +388,7 @@ const users = {
toggleActivationStatus ({ rootState, commit }, { user }) {
const api = user.deactivated ? rootState.api.backendInteractor.activateUser : rootState.api.backendInteractor.deactivateUser
api({ user })
.then(({ deactivated }) => commit('updateActivationStatus', { user, deactivated }))
.then((user) => { let deactivated = !user.is_active; commit('updateActivationStatus', { user, deactivated }) })
},
registerPushNotifications (store) {
const token = store.state.currentUser.credentials

View File

@ -9,11 +9,11 @@ const FOLLOW_IMPORT_URL = '/api/pleroma/follow_import'
const DELETE_ACCOUNT_URL = '/api/pleroma/delete_account'
const CHANGE_EMAIL_URL = '/api/pleroma/change_email'
const CHANGE_PASSWORD_URL = '/api/pleroma/change_password'
const TAG_USER_URL = '/api/pleroma/admin/users/tag'
const PERMISSION_GROUP_URL = (screenName, right) => `/api/pleroma/admin/users/${screenName}/permission_group/${right}`
const ACTIVATE_USER_URL = '/api/pleroma/admin/users/activate'
const DEACTIVATE_USER_URL = '/api/pleroma/admin/users/deactivate'
const ADMIN_USERS_URL = '/api/pleroma/admin/users'
const TAG_USER_URL = '/api/v1/pleroma/admin/users/tag'
const PERMISSION_GROUP_URL = (screenName, right) => `/api/v1/pleroma/admin/users/${screenName}/permission_group/${right}`
const ACTIVATE_USER_URL = '/api/v1/pleroma/admin/users/activate'
const DEACTIVATE_USER_URL = '/api/v1/pleroma/admin/users/deactivate'
const ADMIN_USERS_URL = '/api/v1/pleroma/admin/users'
const SUGGESTIONS_URL = '/api/v1/suggestions'
const NOTIFICATION_SETTINGS_URL = '/api/pleroma/notification_settings'
const NOTIFICATION_READ_URL = '/api/v1/pleroma/notifications/read'
@ -215,7 +215,7 @@ const register = ({ params, credentials }) => {
})
}
const getCaptcha = () => fetch('/api/pleroma/captcha').then(resp => resp.json())
const getCaptcha = () => fetch('/api/v1/pleroma/captcha').then(resp => resp.json())
const authHeaders = (accessToken) => {
if (accessToken) {
@ -304,7 +304,7 @@ const fetchUser = ({ id, credentials }) => {
}
const fetchUserRelationship = ({ id, credentials }) => {
let url = `${MASTODON_USER_RELATIONSHIPS_URL}/?id=${id}`
let url = `${MASTODON_USER_RELATIONSHIPS_URL}/?id=${id.id}`
return fetch(url, { headers: authHeaders(credentials) })
.then((response) => {
return new Promise((resolve, reject) => response.json()
@ -1002,7 +1002,7 @@ const searchUsers = ({ credentials, query }) => {
.then((data) => data.map(parseUser))
}
const search2 = ({ credentials, q, resolve, limit, offset, following }) => {
const search2 = ({ credentials, q, resolve, limit, offset, following, type }) => {
let url = MASTODON_SEARCH_2
let params = []
@ -1026,6 +1026,10 @@ const search2 = ({ credentials, q, resolve, limit, offset, following }) => {
params.push(['following', true])
}
if (type) {
params.push(['following', type])
}
params.push(['with_relationships', true])
let queryString = map(params, (param) => `${param[0]}=${param[1]}`).join('&')

View File

@ -385,13 +385,16 @@ export const parseNotification = (data) => {
if (masto) {
output.type = mastoDict[data.type] || data.type
output.seen = data.pleroma.is_seen
output.status = isStatusNotification(output.type) ? parseStatus(data.status) : null
output.action = output.status // TODO: Refactor, this is unneeded
if (data.status) {
output.status = isStatusNotification(output.type) ? parseStatus(data.status) : null
output.action = output.status // TODO: Refactor, this is unneeded
}
output.target = output.type !== 'move'
? null
: parseUser(data.target)
output.from_profile = parseUser(data.account)
output.emoji = data.emoji
output.emoji_url = data.emoji_url
} else {
const parsedNotice = parseStatus(data.notice)
output.type = data.ntype

View File

@ -56,14 +56,6 @@ const fetchNotifications = ({ store, args, older }) => {
update({ store, notifications, older })
return notifications
})
.catch((error) => {
store.dispatch('pushGlobalNotice', {
level: 'error',
messageKey: 'notifications.error',
messageArgs: [error.message],
timeout: 5000
})
})
}
const startFetching = ({ credentials, store }) => {

View File

@ -65,14 +65,6 @@ const fetchAndUpdate = ({
update({ store, statuses, timeline, showImmediately, userId, pagination })
return { statuses, pagination }
})
.catch((error) => {
store.dispatch('pushGlobalNotice', {
level: 'error',
messageKey: 'timeline.error',
messageArgs: [error.message],
timeout: 5000
})
})
}
const startFetching = ({ timeline = 'friends', credentials, store, userId = false, tag = false }) => {

File diff suppressed because it is too large Load Diff

24448
yarn.lock

File diff suppressed because it is too large Load Diff