Compare commits
58 commits
froth-akko
...
froth-zero
Author | SHA1 | Date | |
---|---|---|---|
440c2c15b6 | |||
|
a6a77233c8 | ||
|
cf92ca25e3 | ||
|
23a3b1a8b3 | ||
f30b28032d | |||
|
8d391fd3d3 | ||
c02e757528 | |||
|
53516bea57 | ||
|
93ea1f0659 | ||
4241b67771 | |||
61fa33739d | |||
c64a627438 | |||
b29f9c4e88 | |||
|
74b1e66dc3 | ||
|
99214ce111 | ||
|
8f99ca066a | ||
|
010675cb80 | ||
|
4176ef4996 | ||
|
2745aabcb4 | ||
|
72efb7d0e9 | ||
|
cc09f0bcb9 | ||
|
655adfdbd8 | ||
|
8cff52ac32 | ||
|
8d3d8de7a3 | ||
|
a6a5f03966 | ||
|
83fa4ad4a7 | ||
|
0ab60d63ef | ||
|
788691b296 | ||
|
fc7cdb1e69 | ||
|
44047458e4 | ||
|
c27845169d | ||
|
378ac642d4 | ||
|
23b60e9263 | ||
|
e1785d2d91 | ||
|
3cc6a0b252 | ||
|
710891d1f3 | ||
|
6170aa3ee7 | ||
|
afb388b73b | ||
|
6729ab8cf1 | ||
|
69671446e5 | ||
d8ec9f596d | |||
|
df8bf08f46 | ||
2d43adb6e5 | |||
|
402f3a61fe | ||
|
58cd2fb994 | ||
|
e32ae82441 | ||
|
0f5e601db2 | ||
|
2f215b8d2e | ||
|
9c827c9834 | ||
|
7622b1230f | ||
|
84ce0046f9 | ||
|
99d2c75a1a | ||
|
fbd9ee03c4 | ||
|
387958c7f1 | ||
|
51cb9b5c8f | ||
|
16772d75f7 | ||
|
241d520795 | ||
|
2ce47e8cf0 |
75 changed files with 19206 additions and 11398 deletions
42
.drone.yml
Normal file
42
.drone.yml
Normal 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
|
16
.eslintrc.js
16
.eslintrc.js
|
@ -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
10
.gitignore
vendored
|
@ -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
1
.yarnrc.yml
Normal file
|
@ -0,0 +1 @@
|
|||
nodeLinker: node-modules
|
|
@ -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
17
ci/add-key.sh
Executable 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
11
ci/deploy.sh
Executable 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"
|
|
@ -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>
|
||||
|
|
19
package.json
19
package.json
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
13
src/App.js
13
src/App.js
|
@ -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: {
|
||||
|
|
|
@ -573,9 +573,10 @@ nav {
|
|||
}
|
||||
|
||||
.main {
|
||||
flex-basis: 50%;
|
||||
flex-basis: 25%;
|
||||
flex-grow: 1;
|
||||
flex-shrink: 1;
|
||||
order: 50;
|
||||
}
|
||||
|
||||
.sidebar-bounds {
|
||||
|
|
24
src/App.vue
24
src/App.vue
|
@ -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
|
||||
|
|
133
src/components/emoji_grid/emoji_grid.js
Normal file
133
src/components/emoji_grid/emoji_grid.js
Normal 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
|
60
src/components/emoji_grid/emoji_grid.scss
Normal file
60
src/components/emoji_grid/emoji_grid.scss
Normal 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%;
|
||||
}
|
||||
}
|
||||
}
|
48
src/components/emoji_grid/emoji_grid.vue
Normal file
48
src/components/emoji_grid/emoji_grid.vue
Normal 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>
|
|
@ -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)
|
||||
}
|
||||
},
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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%;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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') }}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
<button
|
||||
class="btn button-default follow-button"
|
||||
:class="{ toggled: isPressed }"
|
||||
:disabled="inProgress"
|
||||
:disabled="disabled"
|
||||
:title="title"
|
||||
@click="onClick"
|
||||
>
|
||||
|
|
|
@ -20,6 +20,7 @@
|
|||
:relationship="relationship"
|
||||
:label-following="$t('user_card.follow_unfollow')"
|
||||
class="follow-card-follow-button"
|
||||
:user="user"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
|
|
|
@ -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', [])
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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>
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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') }}
|
||||
|
|
|
@ -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
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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
|
|
@ -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>
|
|
@ -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;
|
||||
|
|
|
@ -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í",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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?",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -312,7 +312,6 @@
|
|||
"follow": "עקוב",
|
||||
"follow_sent": "בקשה נשלחה!",
|
||||
"follow_progress": "מבקש…",
|
||||
"follow_again": "שלח בקשה שוב?",
|
||||
"follow_unfollow": "בטל עקיבה",
|
||||
"followees": "נעקבים",
|
||||
"followers": "עוקבים",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -567,7 +567,6 @@
|
|||
"follow": "フォロー",
|
||||
"follow_sent": "リクエストを、おくりました!",
|
||||
"follow_progress": "リクエストしています…",
|
||||
"follow_again": "ふたたびリクエストをおくりますか?",
|
||||
"follow_unfollow": "フォローをやめる",
|
||||
"followees": "フォロー",
|
||||
"followers": "フォロワー",
|
||||
|
|
|
@ -679,7 +679,6 @@
|
|||
"follow": "フォロー",
|
||||
"follow_sent": "リクエストを送りました!",
|
||||
"follow_progress": "リクエストしています…",
|
||||
"follow_again": "再びリクエストを送りますか?",
|
||||
"follow_unfollow": "フォローをやめる",
|
||||
"followees": "フォロー",
|
||||
"followers": "フォロワー",
|
||||
|
|
|
@ -428,7 +428,6 @@
|
|||
"follow": "팔로우",
|
||||
"follow_sent": "요청 보내짐!",
|
||||
"follow_progress": "요청 중…",
|
||||
"follow_again": "요청을 다시 보낼까요?",
|
||||
"follow_unfollow": "팔로우 중지",
|
||||
"followees": "팔로우 중",
|
||||
"followers": "팔로워",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -550,7 +550,6 @@
|
|||
"follow": "Читать",
|
||||
"follow_sent": "Запрос отправлен!",
|
||||
"follow_progress": "Запрашиваем…",
|
||||
"follow_again": "Запросить еще раз?",
|
||||
"follow_unfollow": "Перестать читать",
|
||||
"followees": "Читаемые",
|
||||
"followers": "Читатели",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -743,7 +743,6 @@
|
|||
"message": "Повідомлення",
|
||||
"follow": "Підписатись",
|
||||
"follow_unfollow": "Відписатись",
|
||||
"follow_again": "Відправити запит знову?",
|
||||
"follow_sent": "Запит відправлено!",
|
||||
"blocked": "Заблоковано!",
|
||||
"admin_menu": {
|
||||
|
|
|
@ -672,7 +672,6 @@
|
|||
"follow": "关注",
|
||||
"follow_sent": "请求已发送!",
|
||||
"follow_progress": "请求中…",
|
||||
"follow_again": "再次发送请求?",
|
||||
"follow_unfollow": "取消关注",
|
||||
"followees": "正在关注",
|
||||
"followers": "关注者",
|
||||
|
|
|
@ -766,7 +766,6 @@
|
|||
"follow": "關注",
|
||||
"follow_sent": "請求已發送!",
|
||||
"follow_progress": "請求中…",
|
||||
"follow_again": "再次發送請求?",
|
||||
"follow_unfollow": "取消關注",
|
||||
"followees": "正在關注",
|
||||
"followers": "關注者",
|
||||
|
|
14
src/main.js
14
src/main.js
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 })
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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('&')
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 }) => {
|
||||
|
|
|
@ -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 }) => {
|
||||
|
|
4442
static/emoji.json
4442
static/emoji.json
File diff suppressed because it is too large
Load diff
Loading…
Reference in a new issue