forked from AkkomaGang/akkoma-fe
Merge branch 'develop' into stable
This commit is contained in:
commit
8569b5946e
51 changed files with 799 additions and 387 deletions
12
README.md
12
README.md
|
@ -1,22 +1,22 @@
|
||||||
# Pleroma-FE
|
# Akkoma-FE
|
||||||
|
|
||||||
![English OK](https://img.shields.io/badge/English-OK-blueviolet) ![日本語OK](https://img.shields.io/badge/%E6%97%A5%E6%9C%AC%E8%AA%9E-OK-blueviolet)
|
![English OK](https://img.shields.io/badge/English-OK-blueviolet) ![日本語OK](https://img.shields.io/badge/%E6%97%A5%E6%9C%AC%E8%AA%9E-OK-blueviolet)
|
||||||
|
|
||||||
This is a fork of Pleroma-FE from the Pleroma project, with support for new Akkoma features such as:
|
This is a fork of Akkoma-FE from the Pleroma project, with support for new Akkoma features such as:
|
||||||
- MFM support via [marked-mfm](https://akkoma.dev/sfr/marked-mfm)
|
- MFM support via [marked-mfm](https://akkoma.dev/sfr/marked-mfm)
|
||||||
- Custom emoji reactions
|
- Custom emoji reactions
|
||||||
|
|
||||||
# For Translators
|
# For Translators
|
||||||
|
|
||||||
The [Weblate UI](https://translate.akkoma.dev/projects/akkoma/pleroma-fe/) is recommended for adding or modifying translations for Pleroma-FE.
|
The [Weblate UI](https://translate.akkoma.dev/projects/akkoma/pleroma-fe/) is recommended for adding or modifying translations for Akkoma-FE.
|
||||||
|
|
||||||
Alternatively, edit/create `src/i18n/$LANGUAGE_CODE.json` (where `$LANGUAGE_CODE` is the [ISO 639-1 code](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes) for your language), then add your language to [src/i18n/messages.js](https://akkoma.dev/AkkomaGang/pleroma-fe/src/branch/develop/src/i18n/messages.js) if it doesn't already exist there.
|
Alternatively, edit/create `src/i18n/$LANGUAGE_CODE.json` (where `$LANGUAGE_CODE` is the [ISO 639-1 code](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes) for your language), then add your language to [src/i18n/messages.js](https://akkoma.dev/AkkomaGang/pleroma-fe/src/branch/develop/src/i18n/messages.js) if it doesn't already exist there.
|
||||||
|
|
||||||
Pleroma-FE will set your language by your browser locale, but you can temporarily force it in the code by changing the locale in main.js.
|
Akkoma-FE will set your language by your browser locale, but you can temporarily force it in the code by changing the locale in main.js.
|
||||||
|
|
||||||
# FOR ADMINS
|
# FOR ADMINS
|
||||||
|
|
||||||
To use Pleroma-FE in Akkoma, use the [frontend](https://docs.akkoma.dev/stable/administration/CLI_tasks/frontend/) CLI task to install Pleroma-FE, then modify your configuration as described in the [Frontend Management](https://docs.akkoma.dev/stable/configuration/frontend_management/) doc.
|
To use Akkoma-FE in Akkoma, use the [frontend](https://docs.akkoma.dev/stable/administration/CLI_tasks/frontend/) CLI task to install Akkoma-FE, then modify your configuration as described in the [Frontend Management](https://docs.akkoma.dev/stable/configuration/frontend_management/) doc.
|
||||||
|
|
||||||
## Build Setup
|
## Build Setup
|
||||||
|
|
||||||
|
@ -52,4 +52,4 @@ Edit config.json for configuration.
|
||||||
|
|
||||||
### Login methods
|
### Login methods
|
||||||
|
|
||||||
```loginMethod``` can be set to either ```password``` (the default) or ```token```, which will use the full oauth redirection flow, which is useful for SSO situations.
|
```loginMethod``` can be set to either ```password``` (the default) or ```token```, which will use the full oauth redirection flow, which is useful for SSO situations.
|
||||||
|
|
|
@ -10,6 +10,7 @@
|
||||||
<link rel="stylesheet" href="/static/font/css/lato.css">
|
<link rel="stylesheet" href="/static/font/css/lato.css">
|
||||||
<link rel="stylesheet" href="/static/mfm.css">
|
<link rel="stylesheet" href="/static/mfm.css">
|
||||||
<link rel="stylesheet" href="/static/custom.css">
|
<link rel="stylesheet" href="/static/custom.css">
|
||||||
|
<link rel="stylesheet" href="/static/theme-holder.css" id="theme-holder">
|
||||||
<!--server-generated-meta-->
|
<!--server-generated-meta-->
|
||||||
<link rel="icon" type="image/png" href="/favicon.png">
|
<link rel="icon" type="image/png" href="/favicon.png">
|
||||||
<link rel="manifest" href="/manifest.json">
|
<link rel="manifest" href="/manifest.json">
|
||||||
|
|
|
@ -18,19 +18,21 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "7.17.8",
|
"@babel/runtime": "7.17.8",
|
||||||
"@chenfengyuan/vue-qrcode": "2.0.0",
|
"@chenfengyuan/vue-qrcode": "2.0.0",
|
||||||
|
"@floatingghost/pinch-zoom-element": "^1.3.1",
|
||||||
"@fortawesome/fontawesome-svg-core": "1.3.0",
|
"@fortawesome/fontawesome-svg-core": "1.3.0",
|
||||||
"@fortawesome/free-regular-svg-icons": "^6.1.2",
|
"@fortawesome/free-regular-svg-icons": "^6.1.2",
|
||||||
"@fortawesome/free-solid-svg-icons": "^6.2.0",
|
"@fortawesome/free-solid-svg-icons": "^6.2.0",
|
||||||
"@fortawesome/vue-fontawesome": "3.0.1",
|
"@fortawesome/vue-fontawesome": "3.0.1",
|
||||||
"@kazvmoe-infra/pinch-zoom-element": "1.2.0",
|
|
||||||
"@vuelidate/core": "^2.0.0",
|
"@vuelidate/core": "^2.0.0",
|
||||||
"@vuelidate/validators": "^2.0.0",
|
"@vuelidate/validators": "^2.0.0",
|
||||||
|
"blurhash": "^2.0.4",
|
||||||
"body-scroll-lock": "2.7.1",
|
"body-scroll-lock": "2.7.1",
|
||||||
"chromatism": "3.0.0",
|
"chromatism": "3.0.0",
|
||||||
"click-outside-vue3": "4.0.1",
|
"click-outside-vue3": "4.0.1",
|
||||||
"cropperjs": "1.5.12",
|
"cropperjs": "1.5.12",
|
||||||
"diff": "3.5.0",
|
"diff": "3.5.0",
|
||||||
"escape-html": "1.0.3",
|
"escape-html": "1.0.3",
|
||||||
|
"iso-639-1": "^2.1.15",
|
||||||
"js-cookie": "^3.0.1",
|
"js-cookie": "^3.0.1",
|
||||||
"localforage": "1.10.0",
|
"localforage": "1.10.0",
|
||||||
"parse-link-header": "^2.0.0",
|
"parse-link-header": "^2.0.0",
|
||||||
|
@ -82,7 +84,6 @@
|
||||||
"html-webpack-plugin": "^5.5.0",
|
"html-webpack-plugin": "^5.5.0",
|
||||||
"http-proxy-middleware": "0.21.0",
|
"http-proxy-middleware": "0.21.0",
|
||||||
"inject-loader": "2.0.1",
|
"inject-loader": "2.0.1",
|
||||||
"iso-639-1": "2.1.15",
|
|
||||||
"isparta-loader": "2.0.0",
|
"isparta-loader": "2.0.0",
|
||||||
"json-loader": "0.5.7",
|
"json-loader": "0.5.7",
|
||||||
"karma": "6.3.17",
|
"karma": "6.3.17",
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
// stylelint-disable rscss/class-format
|
// stylelint-disable rscss/class-format
|
||||||
@import './_variables.scss';
|
@import './_variables.scss';
|
||||||
|
@import '@fortawesome/fontawesome-svg-core/styles.css';
|
||||||
|
@import '@floatingghost/pinch-zoom-element/dist/pinch-zoom.css';
|
||||||
:root {
|
:root {
|
||||||
--navbar-height: 3.5rem;
|
--navbar-height: 3.5rem;
|
||||||
--post-line-height: 1.4;
|
--post-line-height: 1.4;
|
||||||
|
|
|
@ -4,6 +4,8 @@ import { createRouter, createWebHistory } from 'vue-router'
|
||||||
import vClickOutside from 'click-outside-vue3'
|
import vClickOutside from 'click-outside-vue3'
|
||||||
|
|
||||||
import { FontAwesomeIcon, FontAwesomeLayers } from '@fortawesome/vue-fontawesome'
|
import { FontAwesomeIcon, FontAwesomeLayers } from '@fortawesome/vue-fontawesome'
|
||||||
|
import { config } from '@fortawesome/fontawesome-svg-core';
|
||||||
|
config.autoAddCss = false
|
||||||
|
|
||||||
import App from '../App.vue'
|
import App from '../App.vue'
|
||||||
import routes from './routes'
|
import routes from './routes'
|
||||||
|
|
|
@ -18,6 +18,7 @@ import {
|
||||||
faPencilAlt,
|
faPencilAlt,
|
||||||
faAlignRight
|
faAlignRight
|
||||||
} from '@fortawesome/free-solid-svg-icons'
|
} from '@fortawesome/free-solid-svg-icons'
|
||||||
|
import Blurhash from '../blurhash/Blurhash.vue'
|
||||||
|
|
||||||
library.add(
|
library.add(
|
||||||
faFile,
|
faFile,
|
||||||
|
@ -63,7 +64,8 @@ const Attachment = {
|
||||||
components: {
|
components: {
|
||||||
Flash,
|
Flash,
|
||||||
StillImage,
|
StillImage,
|
||||||
VideoAttachment
|
VideoAttachment,
|
||||||
|
Blurhash
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
classNames () {
|
classNames () {
|
||||||
|
@ -84,6 +86,9 @@ const Attachment = {
|
||||||
useContainFit () {
|
useContainFit () {
|
||||||
return this.$store.getters.mergedConfig.useContainFit
|
return this.$store.getters.mergedConfig.useContainFit
|
||||||
},
|
},
|
||||||
|
useBlurhash () {
|
||||||
|
return this.$store.getters.mergedConfig.useBlurhash
|
||||||
|
},
|
||||||
placeholderName () {
|
placeholderName () {
|
||||||
if (this.attachment.description === '' || !this.attachment.description) {
|
if (this.attachment.description === '' || !this.attachment.description) {
|
||||||
return this.type.toUpperCase()
|
return this.type.toUpperCase()
|
||||||
|
|
|
@ -64,7 +64,15 @@
|
||||||
:title="attachment.description"
|
:title="attachment.description"
|
||||||
@click.prevent.stop="toggleHidden"
|
@click.prevent.stop="toggleHidden"
|
||||||
>
|
>
|
||||||
|
<Blurhash
|
||||||
|
v-if="useBlurhash && attachment.blurhash"
|
||||||
|
:height="512"
|
||||||
|
:width="1024"
|
||||||
|
:hash="attachment.blurhash"
|
||||||
|
:punch="1"
|
||||||
|
/>
|
||||||
<img
|
<img
|
||||||
|
v-else
|
||||||
:key="nsfwImage"
|
:key="nsfwImage"
|
||||||
class="nsfw"
|
class="nsfw"
|
||||||
:src="nsfwImage"
|
:src="nsfwImage"
|
||||||
|
|
66
src/components/blurhash/Blurhash.vue
Normal file
66
src/components/blurhash/Blurhash.vue
Normal file
|
@ -0,0 +1,66 @@
|
||||||
|
<template>
|
||||||
|
<canvas
|
||||||
|
ref="canvas"
|
||||||
|
class="blurhash"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { decode } from "blurhash";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'Blurhash',
|
||||||
|
props: {
|
||||||
|
hash: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
width: {
|
||||||
|
type: Number,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
height: {
|
||||||
|
type: Number,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
punch: {
|
||||||
|
type: Number,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
canvas: null,
|
||||||
|
ctx: null,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.canvas = this.$refs.canvas;
|
||||||
|
this.ctx = this.canvas.getContext('2d');
|
||||||
|
this.canvas.width = 1024;
|
||||||
|
this.canvas.height = 512;
|
||||||
|
this.draw();
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
draw() {
|
||||||
|
const pixels = decode(this.hash, this.width, this.height, this.punch);
|
||||||
|
const imageData = this.ctx.createImageData(this.width, this.height);
|
||||||
|
imageData.data.set(pixels);
|
||||||
|
this.ctx.putImageData(imageData, 0, 0);
|
||||||
|
fetch("/static/blurhash-overlay.png")
|
||||||
|
.then((response) => response.blob())
|
||||||
|
.then((blob) => {
|
||||||
|
const img = new Image();
|
||||||
|
img.src = URL.createObjectURL(blob);
|
||||||
|
img.onload = () => {
|
||||||
|
this.ctx.drawImage(img, 0, 0, this.width, this.height);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
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>
|
|
@ -205,7 +205,6 @@ const EmojiInput = {
|
||||||
},
|
},
|
||||||
triggerShowPicker () {
|
triggerShowPicker () {
|
||||||
this.showPicker = true
|
this.showPicker = true
|
||||||
this.$refs.picker.startEmojiLoad()
|
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
this.scrollIntoView()
|
this.scrollIntoView()
|
||||||
this.focusPickerInput()
|
this.focusPickerInput()
|
||||||
|
@ -223,7 +222,6 @@ const EmojiInput = {
|
||||||
this.showPicker = !this.showPicker
|
this.showPicker = !this.showPicker
|
||||||
if (this.showPicker) {
|
if (this.showPicker) {
|
||||||
this.scrollIntoView()
|
this.scrollIntoView()
|
||||||
this.$refs.picker.startEmojiLoad()
|
|
||||||
this.$nextTick(this.focusPickerInput)
|
this.$nextTick(this.focusPickerInput)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
@ -18,6 +18,7 @@
|
||||||
<EmojiPicker
|
<EmojiPicker
|
||||||
v-if="enableEmojiPicker"
|
v-if="enableEmojiPicker"
|
||||||
ref="picker"
|
ref="picker"
|
||||||
|
show-keep-open
|
||||||
:class="{ hide: !showPicker }"
|
:class="{ hide: !showPicker }"
|
||||||
:enable-sticker-picker="enableStickerPicker"
|
:enable-sticker-picker="enableStickerPicker"
|
||||||
class="emoji-picker-panel"
|
class="emoji-picker-panel"
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import { defineAsyncComponent } from 'vue'
|
import { defineAsyncComponent } from 'vue'
|
||||||
import Checkbox from '../checkbox/checkbox.vue'
|
import Checkbox from '../checkbox/checkbox.vue'
|
||||||
|
import EmojiGrid from '../emoji_grid/emoji_grid.vue'
|
||||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||||
import {
|
import {
|
||||||
faBoxOpen,
|
faBoxOpen,
|
||||||
|
@ -14,19 +15,17 @@ library.add(
|
||||||
faSmileBeam
|
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 EmojiPicker = {
|
const EmojiPicker = {
|
||||||
props: {
|
props: {
|
||||||
enableStickerPicker: {
|
enableStickerPicker: {
|
||||||
required: false,
|
required: false,
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false
|
default: false
|
||||||
|
},
|
||||||
|
showKeepOpen: {
|
||||||
|
required: false,
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
data () {
|
data () {
|
||||||
|
@ -34,16 +33,13 @@ const EmojiPicker = {
|
||||||
keyword: '',
|
keyword: '',
|
||||||
activeGroup: 'standard',
|
activeGroup: 'standard',
|
||||||
showingStickers: false,
|
showingStickers: false,
|
||||||
groupsScrolledClass: 'scrolled-top',
|
keepOpen: false
|
||||||
keepOpen: false,
|
|
||||||
customEmojiBufferSlice: LOAD_EMOJI_BY,
|
|
||||||
customEmojiTimeout: null,
|
|
||||||
customEmojiLoadAllConfirmed: false
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
components: {
|
components: {
|
||||||
StickerPicker: defineAsyncComponent(() => import('../sticker_picker/sticker_picker.vue')),
|
StickerPicker: defineAsyncComponent(() => import('../sticker_picker/sticker_picker.vue')),
|
||||||
Checkbox
|
Checkbox,
|
||||||
|
EmojiGrid
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
onStickerUploaded (e) {
|
onStickerUploaded (e) {
|
||||||
|
@ -56,12 +52,6 @@ const EmojiPicker = {
|
||||||
const value = emoji.imageUrl ? `:${emoji.displayText}:` : emoji.replacement
|
const value = emoji.imageUrl ? `:${emoji.displayText}:` : emoji.replacement
|
||||||
this.$emit('emoji', { insertion: value, keepOpen: this.keepOpen })
|
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) {
|
onWheel (e) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
this.$refs['emoji-tabs'].scrollBy(e.deltaY, 0)
|
this.$refs['emoji-tabs'].scrollBy(e.deltaY, 0)
|
||||||
|
@ -69,68 +59,12 @@ const EmojiPicker = {
|
||||||
highlight (key) {
|
highlight (key) {
|
||||||
this.setShowStickers(false)
|
this.setShowStickers(false)
|
||||||
this.activeGroup = key
|
this.activeGroup = key
|
||||||
},
|
if (this.keyword.length) {
|
||||||
updateScrolledClass (target) {
|
this.$refs.emojiGrid.scrollToItem(key)
|
||||||
if (target.scrollTop <= 5) {
|
|
||||||
this.groupsScrolledClass = 'scrolled-top'
|
|
||||||
} else if (target.scrollTop >= target.scrollTopMax - 5) {
|
|
||||||
this.groupsScrolledClass = 'scrolled-bottom'
|
|
||||||
} else {
|
|
||||||
this.groupsScrolledClass = 'scrolled-middle'
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
triggerLoadMore (target) {
|
onActiveGroup (group) {
|
||||||
const ref = this.$refs['group-end-custom']
|
this.activeGroup = group
|
||||||
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.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
|
|
||||||
},
|
},
|
||||||
toggleStickers () {
|
toggleStickers () {
|
||||||
this.showingStickers = !this.showingStickers
|
this.showingStickers = !this.showingStickers
|
||||||
|
@ -146,13 +80,6 @@ const EmojiPicker = {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
watch: {
|
|
||||||
keyword () {
|
|
||||||
this.customEmojiLoadAllConfirmed = false
|
|
||||||
this.onScroll()
|
|
||||||
this.startEmojiLoad(true)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
computed: {
|
computed: {
|
||||||
activeGroupView () {
|
activeGroupView () {
|
||||||
return this.showingStickers ? '' : this.activeGroup
|
return this.showingStickers ? '' : this.activeGroup
|
||||||
|
@ -168,9 +95,6 @@ const EmojiPicker = {
|
||||||
this.$store.state.instance.customEmoji || []
|
this.$store.state.instance.customEmoji || []
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
customEmojiBuffer () {
|
|
||||||
return this.filteredEmoji.slice(0, this.customEmojiBufferSlice)
|
|
||||||
},
|
|
||||||
emojis () {
|
emojis () {
|
||||||
const standardEmojis = this.$store.state.instance.emoji || []
|
const standardEmojis = this.$store.state.instance.emoji || []
|
||||||
const customEmojis = this.sortedEmoji
|
const customEmojis = this.sortedEmoji
|
||||||
|
|
|
@ -1,5 +1,16 @@
|
||||||
@import '../../_variables.scss';
|
@import '../../_variables.scss';
|
||||||
|
|
||||||
|
// The worst query selector ever
|
||||||
|
// selects ONLY emojis pickers in replies in notifications
|
||||||
|
// who thought this was a good idea?
|
||||||
|
.notification > .Status > .status-container > .post-status-form > form > .form-group > .emoji-input > .emoji-picker {
|
||||||
|
max-width: 100%;
|
||||||
|
left: 0;
|
||||||
|
@media (min-width: 1300px) {
|
||||||
|
left: -30px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.Notification {
|
.Notification {
|
||||||
.emoji-picker {
|
.emoji-picker {
|
||||||
min-width: 160%;
|
min-width: 160%;
|
||||||
|
@ -7,7 +18,7 @@
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
left: -70%;
|
left: -70%;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
@media (min-width: 800px) and (max-width: 1300px) {
|
@media (min-width: 800px) and (max-width: 1280px) {
|
||||||
left: -50%;
|
left: -50%;
|
||||||
min-width: 50%;
|
min-width: 50%;
|
||||||
max-width: 130%;
|
max-width: 130%;
|
||||||
|
@ -18,6 +29,10 @@
|
||||||
min-width: 50%;
|
min-width: 50%;
|
||||||
max-width: 130%;
|
max-width: 130%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.Status > .emoji-picker {
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.emoji-picker {
|
.emoji-picker {
|
||||||
|
@ -70,10 +85,6 @@
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.emoji-groups {
|
|
||||||
min-height: 200px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.additional-tabs {
|
.additional-tabs {
|
||||||
border-left: 1px solid;
|
border-left: 1px solid;
|
||||||
border-left-color: $fallback--icon;
|
border-left-color: $fallback--icon;
|
||||||
|
@ -152,76 +163,12 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.emoji {
|
.emoji-search {
|
||||||
&-search {
|
padding: 5px;
|
||||||
padding: 5px;
|
flex: 0 0 auto;
|
||||||
flex: 0 0 auto;
|
|
||||||
|
|
||||||
input {
|
input {
|
||||||
width: 100%;
|
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: 0.85em;
|
|
||||||
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%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,9 +2,9 @@
|
||||||
<div class="emoji-picker panel panel-default panel-body">
|
<div class="emoji-picker panel panel-default panel-body">
|
||||||
<div class="heading">
|
<div class="heading">
|
||||||
<span
|
<span
|
||||||
|
ref="emoji-tabs"
|
||||||
class="emoji-tabs"
|
class="emoji-tabs"
|
||||||
@wheel="onWheel"
|
@wheel="onWheel"
|
||||||
ref="emoji-tabs"
|
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
v-for="group in emojis"
|
v-for="group in emojis"
|
||||||
|
@ -51,40 +51,16 @@
|
||||||
@input="$event.target.composing = false"
|
@input="$event.target.composing = false"
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
|
<EmojiGrid
|
||||||
|
ref="emojiGrid"
|
||||||
|
:groups="emojisView"
|
||||||
|
@emoji="onEmoji"
|
||||||
|
@active-group="onActiveGroup"
|
||||||
|
/>
|
||||||
<div
|
<div
|
||||||
ref="emoji-groups"
|
v-if="showKeepOpen"
|
||||||
class="emoji-groups"
|
class="keep-open"
|
||||||
: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>
|
|
||||||
<div class="keep-open">
|
|
||||||
<Checkbox v-model="keepOpen">
|
<Checkbox v-model="keepOpen">
|
||||||
{{ $t('emoji.keep_open') }}
|
{{ $t('emoji.keep_open') }}
|
||||||
</Checkbox>
|
</Checkbox>
|
||||||
|
|
|
@ -19,6 +19,7 @@
|
||||||
:title="reaction.name"
|
:title="reaction.name"
|
||||||
class="reaction-emoji"
|
class="reaction-emoji"
|
||||||
width="2.55em"
|
width="2.55em"
|
||||||
|
height="2.55em"
|
||||||
>
|
>
|
||||||
{{ reaction.count }}
|
{{ reaction.count }}
|
||||||
</span>
|
</span>
|
||||||
|
@ -65,6 +66,7 @@
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
.reaction-emoji {
|
.reaction-emoji {
|
||||||
width: 2.55em !important;
|
width: 2.55em !important;
|
||||||
|
height: 2.55em !important;
|
||||||
margin-right: 0.25em;
|
margin-right: 0.25em;
|
||||||
}
|
}
|
||||||
&:focus {
|
&:focus {
|
||||||
|
|
|
@ -144,6 +144,7 @@ const ExtraButtons = {
|
||||||
statusPoll: this.status.poll,
|
statusPoll: this.status.poll,
|
||||||
statusFiles: [...this.status.attachments],
|
statusFiles: [...this.status.attachments],
|
||||||
statusScope: this.status.visibility,
|
statusScope: this.status.visibility,
|
||||||
|
statusLanguage: this.status.language,
|
||||||
statusContentType: data.content_type
|
statusContentType: data.content_type
|
||||||
}))
|
}))
|
||||||
this.doDeleteStatus()
|
this.doDeleteStatus()
|
||||||
|
|
|
@ -43,6 +43,7 @@ const FollowRequestCard = {
|
||||||
doApprove () {
|
doApprove () {
|
||||||
this.$store.state.api.backendInteractor.approveUser({ id: this.user.id })
|
this.$store.state.api.backendInteractor.approveUser({ id: this.user.id })
|
||||||
this.$store.dispatch('removeFollowRequest', this.user)
|
this.$store.dispatch('removeFollowRequest', this.user)
|
||||||
|
this.$store.dispatch('decrementFollowRequestsCount')
|
||||||
|
|
||||||
const notifId = this.findFollowRequestNotificationId()
|
const notifId = this.findFollowRequestNotificationId()
|
||||||
this.$store.dispatch('markSingleNotificationAsSeen', { id: notifId })
|
this.$store.dispatch('markSingleNotificationAsSeen', { id: notifId })
|
||||||
|
@ -66,6 +67,7 @@ const FollowRequestCard = {
|
||||||
this.$store.state.api.backendInteractor.denyUser({ id: this.user.id })
|
this.$store.state.api.backendInteractor.denyUser({ id: this.user.id })
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this.$store.dispatch('dismissNotificationLocal', { id: notifId })
|
this.$store.dispatch('dismissNotificationLocal', { id: notifId })
|
||||||
|
this.$store.dispatch('decrementFollowRequestsCount')
|
||||||
this.$store.dispatch('removeFollowRequest', this.user)
|
this.$store.dispatch('removeFollowRequest', this.user)
|
||||||
})
|
})
|
||||||
this.hideDenyConfirmDialog()
|
this.hideDenyConfirmDialog()
|
||||||
|
@ -80,6 +82,11 @@ const FollowRequestCard = {
|
||||||
},
|
},
|
||||||
shouldConfirmDeny () {
|
shouldConfirmDeny () {
|
||||||
return this.mergedConfig.modalOnDenyFollow
|
return this.mergedConfig.modalOnDenyFollow
|
||||||
|
},
|
||||||
|
show () {
|
||||||
|
const notifId = this.$store.state.api.followRequests.find(req => req.id === this.user.id)
|
||||||
|
|
||||||
|
return notifId !== undefined
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<template>
|
<template>
|
||||||
<basic-user-card :user="user">
|
<basic-user-card :user="user" v-if="show">
|
||||||
<div class="follow-request-card-content-container">
|
<div class="follow-request-card-content-container">
|
||||||
<button
|
<button
|
||||||
class="btn button-default"
|
class="btn button-default"
|
||||||
|
|
|
@ -1,10 +1,26 @@
|
||||||
import FollowRequestCard from '../follow_request_card/follow_request_card.vue'
|
import FollowRequestCard from '../follow_request_card/follow_request_card.vue'
|
||||||
|
import withLoadMore from '../../hocs/with_load_more/with_load_more'
|
||||||
|
import List from '../list/list.vue'
|
||||||
|
import get from 'lodash/get'
|
||||||
|
|
||||||
|
const FollowRequestList = withLoadMore({
|
||||||
|
fetch: (props, $store) => $store.dispatch('fetchFollowRequests'),
|
||||||
|
select: (props, $store) => get($store.state.api, 'followRequests', []).map(req => $store.getters.findUser(req.id)),
|
||||||
|
destroy: (props, $store) => $store.dispatch('clearFollowRequests'),
|
||||||
|
childPropName: 'items',
|
||||||
|
additionalPropNames: ['userId']
|
||||||
|
})(List);
|
||||||
|
|
||||||
|
|
||||||
const FollowRequests = {
|
const FollowRequests = {
|
||||||
components: {
|
components: {
|
||||||
FollowRequestCard
|
FollowRequestCard,
|
||||||
|
FollowRequestList
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
userId () {
|
||||||
|
return this.$store.state.users.currentUser.id
|
||||||
|
},
|
||||||
requests () {
|
requests () {
|
||||||
return this.$store.state.api.followRequests
|
return this.$store.state.api.followRequests
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,12 +6,11 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="panel-body">
|
<div class="panel-body">
|
||||||
<FollowRequestCard
|
<FollowRequestList :user-id="userId">
|
||||||
v-for="request in requests"
|
<template #item="{item}">
|
||||||
:key="request.id"
|
<FollowRequestCard :user="item" />
|
||||||
:user="request"
|
</template>
|
||||||
class="list-item"
|
</FollowRequestList>
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
77
src/components/followed_tag_card/FollowedTagCard.vue
Normal file
77
src/components/followed_tag_card/FollowedTagCard.vue
Normal file
|
@ -0,0 +1,77 @@
|
||||||
|
<template>
|
||||||
|
<div class="followed-tag-card">
|
||||||
|
<span>
|
||||||
|
<router-link :to="{ name: 'tag-timeline', params: {tag: tag.name}}">
|
||||||
|
<span class="tag-link">#{{ tag.name }}</span>
|
||||||
|
</router-link>
|
||||||
|
<span class="unfollow-tag">
|
||||||
|
<button
|
||||||
|
v-if="isFollowing"
|
||||||
|
class="button-default unfollow-tag-button"
|
||||||
|
:title="$t('user_card.unfollow_tag')"
|
||||||
|
@click="unfollowTag(tag.name)"
|
||||||
|
>
|
||||||
|
{{ $t('user_card.unfollow_tag') }}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
v-else
|
||||||
|
class="button-default follow-tag-button"
|
||||||
|
:title="$t('user_card.follow_tag')"
|
||||||
|
@click="followTag(tag.name)"
|
||||||
|
>
|
||||||
|
{{ $t('user_card.follow_tag') }}
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'FollowedTagCard',
|
||||||
|
props: {
|
||||||
|
tag: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// this is a hack to update the state of the button
|
||||||
|
// for some reason, List does not update on changes to the tag object
|
||||||
|
data: () => ({
|
||||||
|
isFollowing: true
|
||||||
|
}),
|
||||||
|
mounted () {
|
||||||
|
this.isFollowing = this.tag.following
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
unfollowTag (tag) {
|
||||||
|
this.$store.dispatch('unfollowTag', tag)
|
||||||
|
this.isFollowing = false
|
||||||
|
},
|
||||||
|
followTag (tag) {
|
||||||
|
this.$store.dispatch('followTag', tag)
|
||||||
|
this.isFollowing = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.followed-tag-card {
|
||||||
|
margin-left: 1rem;
|
||||||
|
margin-top: 1rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
.unfollow-tag {
|
||||||
|
position: absolute;
|
||||||
|
right: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-link {
|
||||||
|
font-size: large;
|
||||||
|
}
|
||||||
|
|
||||||
|
.unfollow-tag-button, .follow-tag-button {
|
||||||
|
font-size: medium;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -33,11 +33,6 @@ library.add(
|
||||||
)
|
)
|
||||||
|
|
||||||
const NavPanel = {
|
const NavPanel = {
|
||||||
created () {
|
|
||||||
if (this.currentUser && this.currentUser.locked) {
|
|
||||||
this.$store.dispatch('startFetchingFollowRequests')
|
|
||||||
}
|
|
||||||
},
|
|
||||||
components: {
|
components: {
|
||||||
TimelineMenuContent
|
TimelineMenuContent
|
||||||
},
|
},
|
||||||
|
@ -54,11 +49,13 @@ const NavPanel = {
|
||||||
computed: {
|
computed: {
|
||||||
...mapState({
|
...mapState({
|
||||||
currentUser: state => state.users.currentUser,
|
currentUser: state => state.users.currentUser,
|
||||||
followRequestCount: state => state.api.followRequests.length,
|
|
||||||
privateMode: state => state.instance.private,
|
privateMode: state => state.instance.private,
|
||||||
federating: state => state.instance.federating
|
federating: state => state.instance.federating
|
||||||
}),
|
}),
|
||||||
...mapGetters(['unreadAnnouncementCount'])
|
...mapGetters(['unreadAnnouncementCount']),
|
||||||
|
followRequestCount () {
|
||||||
|
return this.$store.state.users.currentUser.follow_requests_count
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import PinchZoom from '@kazvmoe-infra/pinch-zoom-element'
|
import PinchZoom from '@floatingghost/pinch-zoom-element'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
methods: {
|
methods: {
|
||||||
|
|
|
@ -13,6 +13,7 @@ import suggestor from '../emoji_input/suggestor.js'
|
||||||
import { mapGetters, mapState } from 'vuex'
|
import { mapGetters, mapState } from 'vuex'
|
||||||
import Checkbox from '../checkbox/checkbox.vue'
|
import Checkbox from '../checkbox/checkbox.vue'
|
||||||
import Select from '../select/select.vue'
|
import Select from '../select/select.vue'
|
||||||
|
import iso6391 from 'iso-639-1'
|
||||||
|
|
||||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||||
import {
|
import {
|
||||||
|
@ -63,6 +64,7 @@ const PostStatusForm = {
|
||||||
'statusMediaDescriptions',
|
'statusMediaDescriptions',
|
||||||
'statusScope',
|
'statusScope',
|
||||||
'statusContentType',
|
'statusContentType',
|
||||||
|
'statusLanguage',
|
||||||
'replyTo',
|
'replyTo',
|
||||||
'quoteId',
|
'quoteId',
|
||||||
'repliedUser',
|
'repliedUser',
|
||||||
|
@ -128,7 +130,7 @@ const PostStatusForm = {
|
||||||
statusText = buildMentionsString({ user: this.repliedUser, attentions: this.attentions }, currentUser)
|
statusText = buildMentionsString({ user: this.repliedUser, attentions: this.attentions }, currentUser)
|
||||||
}
|
}
|
||||||
|
|
||||||
const { postContentType: contentType, sensitiveByDefault, sensitiveIfSubject } = this.$store.getters.mergedConfig
|
const { postContentType: contentType, sensitiveByDefault, sensitiveIfSubject, interfaceLanguage } = this.$store.getters.mergedConfig
|
||||||
|
|
||||||
let statusParams = {
|
let statusParams = {
|
||||||
spoilerText: this.subject || '',
|
spoilerText: this.subject || '',
|
||||||
|
@ -139,6 +141,7 @@ const PostStatusForm = {
|
||||||
poll: {},
|
poll: {},
|
||||||
mediaDescriptions: {},
|
mediaDescriptions: {},
|
||||||
visibility: this.suggestedVisibility(),
|
visibility: this.suggestedVisibility(),
|
||||||
|
language: interfaceLanguage,
|
||||||
contentType
|
contentType
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -153,6 +156,7 @@ const PostStatusForm = {
|
||||||
poll: this.statusPoll || {},
|
poll: this.statusPoll || {},
|
||||||
mediaDescriptions: this.statusMediaDescriptions || {},
|
mediaDescriptions: this.statusMediaDescriptions || {},
|
||||||
visibility: this.statusScope || this.suggestedVisibility(),
|
visibility: this.statusScope || this.suggestedVisibility(),
|
||||||
|
language: this.statusLanguage || interfaceLanguage,
|
||||||
contentType: statusContentType
|
contentType: statusContentType
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -259,7 +263,10 @@ const PostStatusForm = {
|
||||||
...mapGetters(['mergedConfig']),
|
...mapGetters(['mergedConfig']),
|
||||||
...mapState({
|
...mapState({
|
||||||
mobileLayout: state => state.interface.mobileLayout
|
mobileLayout: state => state.interface.mobileLayout
|
||||||
})
|
}),
|
||||||
|
isoLanguages () {
|
||||||
|
return iso6391.getAllCodes();
|
||||||
|
}
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
'newStatus': {
|
'newStatus': {
|
||||||
|
@ -282,6 +289,7 @@ const PostStatusForm = {
|
||||||
files: [],
|
files: [],
|
||||||
visibility: newStatus.visibility,
|
visibility: newStatus.visibility,
|
||||||
contentType: newStatus.contentType,
|
contentType: newStatus.contentType,
|
||||||
|
language: newStatus.language,
|
||||||
poll: {},
|
poll: {},
|
||||||
mediaDescriptions: {}
|
mediaDescriptions: {}
|
||||||
}
|
}
|
||||||
|
@ -341,6 +349,7 @@ const PostStatusForm = {
|
||||||
inReplyToStatusId: this.replyTo,
|
inReplyToStatusId: this.replyTo,
|
||||||
quoteId: this.quoteId,
|
quoteId: this.quoteId,
|
||||||
contentType: newStatus.contentType,
|
contentType: newStatus.contentType,
|
||||||
|
language: newStatus.language,
|
||||||
poll,
|
poll,
|
||||||
idempotencyKey: this.idempotencyKey
|
idempotencyKey: this.idempotencyKey
|
||||||
}
|
}
|
||||||
|
@ -375,6 +384,7 @@ const PostStatusForm = {
|
||||||
inReplyToStatusId: this.replyTo,
|
inReplyToStatusId: this.replyTo,
|
||||||
quoteId: this.quoteId,
|
quoteId: this.quoteId,
|
||||||
contentType: newStatus.contentType,
|
contentType: newStatus.contentType,
|
||||||
|
language: newStatus.language,
|
||||||
poll: {},
|
poll: {},
|
||||||
preview: true
|
preview: true
|
||||||
}).then((data) => {
|
}).then((data) => {
|
||||||
|
|
|
@ -194,6 +194,23 @@
|
||||||
:on-scope-change="changeVis"
|
:on-scope-change="changeVis"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="language-selector"
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
id="post-language"
|
||||||
|
v-model="newStatus.language"
|
||||||
|
class="form-control"
|
||||||
|
>
|
||||||
|
<option
|
||||||
|
v-for="language in isoLanguages"
|
||||||
|
:key="language"
|
||||||
|
:value="language"
|
||||||
|
>
|
||||||
|
{{ language }}
|
||||||
|
</option>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="postFormats.length > 1"
|
v-if="postFormats.length > 1"
|
||||||
class="text-format"
|
class="text-format"
|
||||||
|
|
|
@ -407,6 +407,15 @@
|
||||||
{{ $t('settings.preload_images') }}
|
{{ $t('settings.preload_images') }}
|
||||||
</BooleanSetting>
|
</BooleanSetting>
|
||||||
</li>
|
</li>
|
||||||
|
<li>
|
||||||
|
<BooleanSetting
|
||||||
|
path="useBlurhash"
|
||||||
|
expert="1"
|
||||||
|
:disabled="!hideNsfw"
|
||||||
|
>
|
||||||
|
{{ $t('settings.use_blurhash') }}
|
||||||
|
</BooleanSetting>
|
||||||
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<BooleanSetting
|
<BooleanSetting
|
||||||
path="useOneClickNsfw"
|
path="useOneClickNsfw"
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
.user-card {
|
.user-card {
|
||||||
position: relative;
|
position: relative;
|
||||||
z-index: 1;
|
z-index: 10;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
--_still-image-img-visibility: visible;
|
--_still-image-img-visibility: visible;
|
||||||
|
@ -235,7 +235,7 @@
|
||||||
line-height: 22px;
|
line-height: 22px;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
|
|
||||||
.following, .requested_by {
|
.following, .requested_by, .blocking {
|
||||||
flex: 1 0 auto;
|
flex: 1 0 auto;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
margin-bottom: .25em;
|
margin-bottom: .25em;
|
||||||
|
|
|
@ -127,6 +127,12 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="user-meta">
|
<div class="user-meta">
|
||||||
|
<div
|
||||||
|
v-if="relationship.blocked_by && loggedIn && isOtherUser"
|
||||||
|
class="blocking"
|
||||||
|
>
|
||||||
|
{{ $t('user_card.blocks_you') }}
|
||||||
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="relationship.followed_by && loggedIn && isOtherUser"
|
v-if="relationship.followed_by && loggedIn && isOtherUser"
|
||||||
class="following"
|
class="following"
|
||||||
|
@ -187,6 +193,7 @@
|
||||||
<FollowButton
|
<FollowButton
|
||||||
:relationship="relationship"
|
:relationship="relationship"
|
||||||
:user="user"
|
:user="user"
|
||||||
|
:disabled="relationship.blocked_by"
|
||||||
/>
|
/>
|
||||||
<template v-if="relationship.following">
|
<template v-if="relationship.following">
|
||||||
<ProgressButton
|
<ProgressButton
|
||||||
|
|
|
@ -10,11 +10,14 @@ import withLoadMore from '../../hocs/with_load_more/with_load_more'
|
||||||
import { debounce } from 'lodash'
|
import { debounce } from 'lodash'
|
||||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||||
import {
|
import {
|
||||||
faCircleNotch
|
faCircleNotch,
|
||||||
|
faCircleCheck
|
||||||
} from '@fortawesome/free-solid-svg-icons'
|
} from '@fortawesome/free-solid-svg-icons'
|
||||||
|
import FollowedTagCard from '../followed_tag_card/FollowedTagCard.vue'
|
||||||
|
|
||||||
library.add(
|
library.add(
|
||||||
faCircleNotch
|
faCircleNotch,
|
||||||
|
faCircleCheck
|
||||||
)
|
)
|
||||||
|
|
||||||
const FollowerList = withLoadMore({
|
const FollowerList = withLoadMore({
|
||||||
|
@ -33,6 +36,14 @@ const FriendList = withLoadMore({
|
||||||
additionalPropNames: ['userId']
|
additionalPropNames: ['userId']
|
||||||
})(List)
|
})(List)
|
||||||
|
|
||||||
|
const FollowedTagList = withLoadMore({
|
||||||
|
fetch: (props, $store) => $store.dispatch('fetchFollowedTags', props.userId),
|
||||||
|
select: (props, $store) => get($store.getters.findUser(props.userId), 'followedTagIds', []).map(id => $store.getters.findTag(id)),
|
||||||
|
destroy: (props, $store) => $store.dispatch('clearFollowedTags', props.userId),
|
||||||
|
childPropName: 'items',
|
||||||
|
additionalPropNames: ['userId']
|
||||||
|
})(List)
|
||||||
|
|
||||||
const isUserPage = ({ name }) => name === 'user-profile' || name === 'external-user-profile'
|
const isUserPage = ({ name }) => name === 'user-profile' || name === 'external-user-profile'
|
||||||
|
|
||||||
const UserProfile = {
|
const UserProfile = {
|
||||||
|
@ -41,6 +52,7 @@ const UserProfile = {
|
||||||
error: false,
|
error: false,
|
||||||
userId: null,
|
userId: null,
|
||||||
tab: 'statuses',
|
tab: 'statuses',
|
||||||
|
followsTab: 'users',
|
||||||
footerRef: null,
|
footerRef: null,
|
||||||
note: null,
|
note: null,
|
||||||
noteLoading: false
|
noteLoading: false
|
||||||
|
@ -165,6 +177,9 @@ const UserProfile = {
|
||||||
this.tab = tab
|
this.tab = tab
|
||||||
this.$router.replace({ hash: `#${tab}` })
|
this.$router.replace({ hash: `#${tab}` })
|
||||||
},
|
},
|
||||||
|
onFollowsTabSwitch (tab) {
|
||||||
|
this.followsTab = tab
|
||||||
|
},
|
||||||
linkClicked ({ target }) {
|
linkClicked ({ target }) {
|
||||||
if (target.tagName === 'SPAN') {
|
if (target.tagName === 'SPAN') {
|
||||||
target = target.parentNode
|
target = target.parentNode
|
||||||
|
@ -200,6 +215,7 @@ const UserProfile = {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
components: {
|
components: {
|
||||||
|
FollowedTagCard,
|
||||||
UserCard,
|
UserCard,
|
||||||
Timeline,
|
Timeline,
|
||||||
FollowerList,
|
FollowerList,
|
||||||
|
@ -207,7 +223,8 @@ const UserProfile = {
|
||||||
FollowCard,
|
FollowCard,
|
||||||
TabSwitcher,
|
TabSwitcher,
|
||||||
Conversation,
|
Conversation,
|
||||||
RichContent
|
RichContent,
|
||||||
|
FollowedTagList
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -37,6 +37,15 @@
|
||||||
:html="field.value"
|
:html="field.value"
|
||||||
:emoji="user.emoji"
|
:emoji="user.emoji"
|
||||||
/>
|
/>
|
||||||
|
<span
|
||||||
|
v-if="field.verified_at"
|
||||||
|
class="user-profile-field-validated"
|
||||||
|
>
|
||||||
|
<FAIcon
|
||||||
|
icon="check-circle"
|
||||||
|
:title="$t('user_profile.field_validated')"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
</dd>
|
</dd>
|
||||||
</dl>
|
</dl>
|
||||||
</div>
|
</div>
|
||||||
|
@ -95,22 +104,48 @@
|
||||||
v-if="followsTabVisible"
|
v-if="followsTabVisible"
|
||||||
key="followees"
|
key="followees"
|
||||||
:label="$t('user_card.followees')"
|
:label="$t('user_card.followees')"
|
||||||
:disabled="!user.friends_count"
|
|
||||||
>
|
>
|
||||||
<FriendList :user-id="userId">
|
<tab-switcher
|
||||||
<template v-slot:item="{item}">
|
:active-tab="followsTab"
|
||||||
<FollowCard :user="item" />
|
:render-only-focused="true"
|
||||||
</template>
|
:on-switch="onFollowsTabSwitch"
|
||||||
</FriendList>
|
>
|
||||||
|
<div
|
||||||
|
key="users"
|
||||||
|
:label="$t('user_card.followed_users')"
|
||||||
|
>
|
||||||
|
<FriendList :user-id="userId">
|
||||||
|
<template #item="{item}">
|
||||||
|
<FollowCard :user="item" />
|
||||||
|
</template>
|
||||||
|
</FriendList>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
key="tags"
|
||||||
|
v-if="isUs"
|
||||||
|
:label="$t('user_card.followed_tags')"
|
||||||
|
>
|
||||||
|
<FollowedTagList
|
||||||
|
:user-id="userId"
|
||||||
|
:get-key="(item) => item.name"
|
||||||
|
>
|
||||||
|
<template #item="{item}">
|
||||||
|
<FollowedTagCard :tag="item" />
|
||||||
|
</template>
|
||||||
|
<template #empty>
|
||||||
|
{{ $t('user_card.not_following_any_hashtags')}}
|
||||||
|
</template>
|
||||||
|
</FollowedTagList>
|
||||||
|
</div>
|
||||||
|
</tab-switcher>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="followersTabVisible"
|
v-if="followersTabVisible"
|
||||||
key="followers"
|
key="followers"
|
||||||
:label="$t('user_card.followers')"
|
:label="$t('user_card.followers')"
|
||||||
:disabled="!user.followers_count"
|
|
||||||
>
|
>
|
||||||
<FollowerList :user-id="userId">
|
<FollowerList :user-id="userId">
|
||||||
<template v-slot:item="{item}">
|
<template #item="{item}">
|
||||||
<FollowCard
|
<FollowCard
|
||||||
:user="item"
|
:user="item"
|
||||||
:no-follows-you="isUs"
|
:no-follows-you="isUs"
|
||||||
|
@ -225,6 +260,11 @@
|
||||||
padding: 0.5em 1.5em;
|
padding: 0.5em 1.5em;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.user-profile-field-validated {
|
||||||
|
margin-left: 1rem;
|
||||||
|
color: green;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -59,7 +59,8 @@ const withLoadMore = ({
|
||||||
this.loading = false
|
this.loading = false
|
||||||
this.bottomedOut = isEmpty(newEntries)
|
this.bottomedOut = isEmpty(newEntries)
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch((e) => {
|
||||||
|
console.error(e)
|
||||||
this.loading = false
|
this.loading = false
|
||||||
this.error = true
|
this.error = true
|
||||||
})
|
})
|
||||||
|
@ -88,7 +89,7 @@ const withLoadMore = ({
|
||||||
const children = this.$slots
|
const children = this.$slots
|
||||||
return (
|
return (
|
||||||
<div class="with-load-more">
|
<div class="with-load-more">
|
||||||
<WrappedComponent {...props}>
|
<WrappedComponent {...props} >
|
||||||
{children}
|
{children}
|
||||||
</WrappedComponent>
|
</WrappedComponent>
|
||||||
<div class="with-load-more-footer">
|
<div class="with-load-more-footer">
|
||||||
|
|
|
@ -939,6 +939,7 @@
|
||||||
"title": "Version"
|
"title": "Version"
|
||||||
},
|
},
|
||||||
"virtual_scrolling": "Optimize timeline rendering",
|
"virtual_scrolling": "Optimize timeline rendering",
|
||||||
|
"use_blurhash": "Use blurhashes for NSFW thumbnails",
|
||||||
"word_filter": "Word filter",
|
"word_filter": "Word filter",
|
||||||
"wordfilter": "Wordfilter"
|
"wordfilter": "Wordfilter"
|
||||||
},
|
},
|
||||||
|
@ -1056,6 +1057,7 @@
|
||||||
"show_new": "Show new",
|
"show_new": "Show new",
|
||||||
"socket_broke": "Realtime connection lost: CloseEvent code {0}",
|
"socket_broke": "Realtime connection lost: CloseEvent code {0}",
|
||||||
"socket_reconnected": "Realtime connection established",
|
"socket_reconnected": "Realtime connection established",
|
||||||
|
"follow_tag": "Follow hashtag",
|
||||||
"unfollow_tag": "Unfollow hashtag",
|
"unfollow_tag": "Unfollow hashtag",
|
||||||
"up_to_date": "Up-to-date"
|
"up_to_date": "Up-to-date"
|
||||||
},
|
},
|
||||||
|
@ -1121,6 +1123,7 @@
|
||||||
"block_confirm_title": "Block user",
|
"block_confirm_title": "Block user",
|
||||||
"block_progress": "Blocking…",
|
"block_progress": "Blocking…",
|
||||||
"blocked": "Blocked!",
|
"blocked": "Blocked!",
|
||||||
|
"blocks_you": "Blocks you!",
|
||||||
"bot": "Bot",
|
"bot": "Bot",
|
||||||
"deactivated": "Deactivated",
|
"deactivated": "Deactivated",
|
||||||
"deny": "Deny",
|
"deny": "Deny",
|
||||||
|
@ -1138,6 +1141,8 @@
|
||||||
"follow_unfollow": "Unfollow",
|
"follow_unfollow": "Unfollow",
|
||||||
"followees": "Following",
|
"followees": "Following",
|
||||||
"followers": "Followers",
|
"followers": "Followers",
|
||||||
|
"followed_tags": "Followed hashtags",
|
||||||
|
"followed_users": "Followed users",
|
||||||
"following": "Following!",
|
"following": "Following!",
|
||||||
"follows_you": "Follows you!",
|
"follows_you": "Follows you!",
|
||||||
"hidden": "Hidden",
|
"hidden": "Hidden",
|
||||||
|
@ -1176,6 +1181,9 @@
|
||||||
"unfollow_confirm_accept_button": "Yes, unfollow",
|
"unfollow_confirm_accept_button": "Yes, unfollow",
|
||||||
"unfollow_confirm_cancel_button": "No, don't unfollow",
|
"unfollow_confirm_cancel_button": "No, don't unfollow",
|
||||||
"unfollow_confirm_title": "Unfollow user",
|
"unfollow_confirm_title": "Unfollow user",
|
||||||
|
"not_following_any_hashtags": "You are not following any hashtags",
|
||||||
|
"follow_tag": "Follow hashtag",
|
||||||
|
"unfollow_tag": "Unfollow hashtag",
|
||||||
"unmute": "Unmute",
|
"unmute": "Unmute",
|
||||||
"unmute_progress": "Unmuting…",
|
"unmute_progress": "Unmuting…",
|
||||||
"unsubscribe": "Unsubscribe"
|
"unsubscribe": "Unsubscribe"
|
||||||
|
@ -1183,7 +1191,8 @@
|
||||||
"user_profile": {
|
"user_profile": {
|
||||||
"profile_does_not_exist": "Sorry, this profile does not exist.",
|
"profile_does_not_exist": "Sorry, this profile does not exist.",
|
||||||
"profile_loading_error": "Sorry, there was an error loading this profile.",
|
"profile_loading_error": "Sorry, there was an error loading this profile.",
|
||||||
"timeline_title": "User timeline"
|
"timeline_title": "User timeline",
|
||||||
|
"field_validated": "Link Verified"
|
||||||
},
|
},
|
||||||
"user_reporting": {
|
"user_reporting": {
|
||||||
"add_comment_description": "The report will be sent to your instance moderators. You can provide an explanation of why you are reporting this account below:",
|
"add_comment_description": "The report will be sent to your instance moderators. You can provide an explanation of why you are reporting this account below:",
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import backendInteractorService from '../services/backend_interactor_service/backend_interactor_service.js'
|
import backendInteractorService from '../services/backend_interactor_service/backend_interactor_service.js'
|
||||||
import { WSConnectionStatus } from '../services/api/api.service.js'
|
import { WSConnectionStatus } from '../services/api/api.service.js'
|
||||||
|
import { map } from 'lodash'
|
||||||
|
|
||||||
const retryTimeout = (multiplier) => 1000 * multiplier
|
const retryTimeout = (multiplier) => 1000 * multiplier
|
||||||
|
|
||||||
|
@ -40,9 +41,6 @@ const api = {
|
||||||
setSocket (state, socket) {
|
setSocket (state, socket) {
|
||||||
state.socket = socket
|
state.socket = socket
|
||||||
},
|
},
|
||||||
setFollowRequests (state, value) {
|
|
||||||
state.followRequests = value
|
|
||||||
},
|
|
||||||
setMastoUserSocketStatus (state, value) {
|
setMastoUserSocketStatus (state, value) {
|
||||||
state.mastoUserSocketStatus = value
|
state.mastoUserSocketStatus = value
|
||||||
},
|
},
|
||||||
|
@ -51,6 +49,15 @@ const api = {
|
||||||
},
|
},
|
||||||
resetRetryMultiplier (state) {
|
resetRetryMultiplier (state) {
|
||||||
state.retryMultiplier = 1
|
state.retryMultiplier = 1
|
||||||
|
},
|
||||||
|
setFollowRequests (state, value) {
|
||||||
|
state.followRequests = [...value]
|
||||||
|
},
|
||||||
|
saveFollowRequests (state, requests) {
|
||||||
|
state.followRequests = [...state.followRequests, ...requests]
|
||||||
|
},
|
||||||
|
saveFollowRequestPagination (state, pagination) {
|
||||||
|
state.followRequestsPagination = pagination
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
actions: {
|
actions: {
|
||||||
|
@ -240,24 +247,22 @@ const api = {
|
||||||
...rest
|
...rest
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
// Follow requests
|
|
||||||
startFetchingFollowRequests (store) {
|
|
||||||
if (store.state.fetchers['followRequests']) return
|
|
||||||
const fetcher = store.state.backendInteractor.startFetchingFollowRequests({ store })
|
|
||||||
|
|
||||||
store.commit('addFetcher', { fetcherName: 'followRequests', fetcher })
|
|
||||||
},
|
|
||||||
stopFetchingFollowRequests (store) {
|
|
||||||
const fetcher = store.state.fetchers.followRequests
|
|
||||||
if (!fetcher) return
|
|
||||||
store.commit('removeFetcher', { fetcherName: 'followRequests', fetcher })
|
|
||||||
},
|
|
||||||
removeFollowRequest (store, request) {
|
removeFollowRequest (store, request) {
|
||||||
let requests = store.state.followRequests.filter((it) => it !== request)
|
let requests = [...store.state.followRequests].filter((it) => it.id !== request.id)
|
||||||
store.commit('setFollowRequests', requests)
|
store.commit('setFollowRequests', requests)
|
||||||
},
|
},
|
||||||
|
fetchFollowRequests ({ rootState, commit }) {
|
||||||
|
const pagination = rootState.api.followRequestsPagination
|
||||||
|
return rootState.api.backendInteractor.getFollowRequests({ pagination })
|
||||||
|
.then((requests) => {
|
||||||
|
if (requests.data.length > 0) {
|
||||||
|
commit('addNewUsers', requests.data)
|
||||||
|
commit('saveFollowRequests', requests.data)
|
||||||
|
commit('saveFollowRequestPagination', requests.pagination)
|
||||||
|
}
|
||||||
|
return requests
|
||||||
|
})
|
||||||
|
},
|
||||||
// Lists
|
// Lists
|
||||||
startFetchingLists (store) {
|
startFetchingLists (store) {
|
||||||
if (store.state.fetchers['lists']) return
|
if (store.state.fetchers['lists']) return
|
||||||
|
|
|
@ -117,7 +117,8 @@ export const defaultState = {
|
||||||
maxDepthInThread: undefined, // instance default
|
maxDepthInThread: undefined, // instance default
|
||||||
translationLanguage: undefined, // instance default,
|
translationLanguage: undefined, // instance default,
|
||||||
supportedTranslationLanguages: {}, // instance default
|
supportedTranslationLanguages: {}, // instance default
|
||||||
userProfileDefaultTab: 'statuses'
|
userProfileDefaultTab: 'statuses',
|
||||||
|
useBlurhash: true,
|
||||||
}
|
}
|
||||||
|
|
||||||
// caching the instance default properties
|
// caching the instance default properties
|
||||||
|
|
|
@ -178,7 +178,7 @@ const instance = {
|
||||||
|
|
||||||
async getCustomEmoji ({ commit, state }) {
|
async getCustomEmoji ({ commit, state }) {
|
||||||
try {
|
try {
|
||||||
const res = await window.fetch('/api/pleroma/emoji.json')
|
const res = await window.fetch('/api/v1/pleroma/emoji')
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
const result = await res.json()
|
const result = await res.json()
|
||||||
const values = Array.isArray(result) ? Object.assign({}, ...result) : result
|
const values = Array.isArray(result) ? Object.assign({}, ...result) : result
|
||||||
|
|
|
@ -186,7 +186,7 @@ const interfaceMod = {
|
||||||
if (thirdColumnMode === 'none' || !rootState.users.currentUser) {
|
if (thirdColumnMode === 'none' || !rootState.users.currentUser) {
|
||||||
commit('setLayoutType', normalOrMobile)
|
commit('setLayoutType', normalOrMobile)
|
||||||
} else {
|
} else {
|
||||||
const wideLayout = width >= 1300
|
const wideLayout = width >= 1280
|
||||||
commit('setLayoutType', wideLayout ? 'wide' : normalOrMobile)
|
commit('setLayoutType', wideLayout ? 'wide' : normalOrMobile)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
@ -2,9 +2,15 @@ import { merge } from 'lodash'
|
||||||
|
|
||||||
const tags = {
|
const tags = {
|
||||||
state: {
|
state: {
|
||||||
// Contains key = id, value = number of trackers for this poll
|
// Contains key = name, value = tag json
|
||||||
tags: {}
|
tags: {}
|
||||||
},
|
},
|
||||||
|
getters: {
|
||||||
|
findTag: state => query => {
|
||||||
|
const result = state.tags[query]
|
||||||
|
return result
|
||||||
|
},
|
||||||
|
},
|
||||||
mutations: {
|
mutations: {
|
||||||
setTag (state, { name, data }) {
|
setTag (state, { name, data }) {
|
||||||
state.tags[name] = data
|
state.tags[name] = data
|
||||||
|
@ -17,17 +23,17 @@ const tags = {
|
||||||
return tag
|
return tag
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
followTag (store, tagName) {
|
followTag ({ rootState, commit }, tagName) {
|
||||||
return store.rootState.api.backendInteractor.followHashtag({ tag: tagName })
|
return rootState.api.backendInteractor.followHashtag({ tag: tagName })
|
||||||
.then((resp) => {
|
.then((resp) => {
|
||||||
store.commit('setTag', { name: tagName, data: resp })
|
commit('setTag', { name: tagName, data: resp })
|
||||||
return resp
|
return resp
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
unfollowTag ({ rootState, commit }, tag) {
|
unfollowTag ({ rootState, commit }, tagName) {
|
||||||
return rootState.api.backendInteractor.unfollowHashtag({ tag })
|
return rootState.api.backendInteractor.unfollowHashtag({ tag: tagName })
|
||||||
.then((resp) => {
|
.then((resp) => {
|
||||||
commit('setTag', { name: tag, data: resp })
|
commit('setTag', { name: tagName, data: resp })
|
||||||
return resp
|
return resp
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,9 +5,9 @@ import { compact, map, each, mergeWith, last, concat, uniq, isArray } from 'loda
|
||||||
import { registerPushNotifications, unregisterPushNotifications } from '../services/push/push.js'
|
import { registerPushNotifications, unregisterPushNotifications } from '../services/push/push.js'
|
||||||
|
|
||||||
// TODO: Unify with mergeOrAdd in statuses.js
|
// TODO: Unify with mergeOrAdd in statuses.js
|
||||||
export const mergeOrAdd = (arr, obj, item) => {
|
export const mergeOrAdd = (arr, obj, item, key = 'id') => {
|
||||||
if (!item) { return false }
|
if (!item) { return false }
|
||||||
const oldItem = obj[item.id]
|
const oldItem = obj[item[key]]
|
||||||
if (oldItem) {
|
if (oldItem) {
|
||||||
// We already have this, so only merge the new info.
|
// We already have this, so only merge the new info.
|
||||||
mergeWith(oldItem, item, mergeArrayLength)
|
mergeWith(oldItem, item, mergeArrayLength)
|
||||||
|
@ -15,7 +15,7 @@ export const mergeOrAdd = (arr, obj, item) => {
|
||||||
} else {
|
} else {
|
||||||
// This is a new item, prepare it
|
// This is a new item, prepare it
|
||||||
arr.push(item)
|
arr.push(item)
|
||||||
obj[item.id] = item
|
obj[item[key]] = item
|
||||||
if (item.screen_name && !item.screen_name.includes('@')) {
|
if (item.screen_name && !item.screen_name.includes('@')) {
|
||||||
obj[item.screen_name.toLowerCase()] = item
|
obj[item.screen_name.toLowerCase()] = item
|
||||||
}
|
}
|
||||||
|
@ -157,6 +157,14 @@ export const mutations = {
|
||||||
const user = state.usersObject[id]
|
const user = state.usersObject[id]
|
||||||
user.followerIds = uniq(concat(user.followerIds || [], followerIds))
|
user.followerIds = uniq(concat(user.followerIds || [], followerIds))
|
||||||
},
|
},
|
||||||
|
saveFollowedTagIds (state, { id, followedTagIds }) {
|
||||||
|
const user = state.usersObject[id]
|
||||||
|
user.followedTagIds = uniq(concat(user.followedTagIds || [], followedTagIds))
|
||||||
|
},
|
||||||
|
saveFollowedTagPagination (state, { id, pagination }) {
|
||||||
|
const user = state.usersObject[id]
|
||||||
|
user.followedTagPagination = pagination
|
||||||
|
},
|
||||||
// Because frontend doesn't have a reason to keep these stuff in memory
|
// Because frontend doesn't have a reason to keep these stuff in memory
|
||||||
// outside of viewing someones user profile.
|
// outside of viewing someones user profile.
|
||||||
clearFriends (state, userId) {
|
clearFriends (state, userId) {
|
||||||
|
@ -171,6 +179,12 @@ export const mutations = {
|
||||||
user['followerIds'] = []
|
user['followerIds'] = []
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
clearFollowedTags (state, userId) {
|
||||||
|
const user = state.usersObject[userId]
|
||||||
|
if (user) {
|
||||||
|
user['followedTagIds'] = []
|
||||||
|
}
|
||||||
|
},
|
||||||
addNewUsers (state, users) {
|
addNewUsers (state, users) {
|
||||||
each(users, (user) => {
|
each(users, (user) => {
|
||||||
if (user.relationship) {
|
if (user.relationship) {
|
||||||
|
@ -251,6 +265,12 @@ export const mutations = {
|
||||||
signUpFailure (state, errors) {
|
signUpFailure (state, errors) {
|
||||||
state.signUpPending = false
|
state.signUpPending = false
|
||||||
state.signUpErrors = errors
|
state.signUpErrors = errors
|
||||||
|
},
|
||||||
|
decrementFollowRequestsCount (store) {
|
||||||
|
store.currentUser.follow_requests_count--
|
||||||
|
},
|
||||||
|
incrementFollowRequestsCount (store) {
|
||||||
|
store.currentUser.follow_requests_count++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -271,7 +291,7 @@ export const getters = {
|
||||||
relationship: state => id => {
|
relationship: state => id => {
|
||||||
const rel = id && state.relationships[id]
|
const rel = id && state.relationships[id]
|
||||||
return rel || { id, loading: true }
|
return rel || { id, loading: true }
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
export const defaultState = {
|
export const defaultState = {
|
||||||
|
@ -282,7 +302,9 @@ export const defaultState = {
|
||||||
usersObject: {},
|
usersObject: {},
|
||||||
signUpPending: false,
|
signUpPending: false,
|
||||||
signUpErrors: [],
|
signUpErrors: [],
|
||||||
relationships: {}
|
relationships: {},
|
||||||
|
tags: [],
|
||||||
|
tagsObject: {}
|
||||||
}
|
}
|
||||||
|
|
||||||
const users = {
|
const users = {
|
||||||
|
@ -402,12 +424,27 @@ const users = {
|
||||||
return followers
|
return followers
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
fetchFollowedTags ({ rootState, commit }, id) {
|
||||||
|
const user = rootState.users.usersObject[id]
|
||||||
|
const pagination = user.followedTagPagination
|
||||||
|
|
||||||
|
return rootState.api.backendInteractor.getFollowedHashtags({ pagination })
|
||||||
|
.then(({ data: tags, pagination }) => {
|
||||||
|
each(tags, tag => commit('setTag', { name: tag.name, data: tag }))
|
||||||
|
commit('saveFollowedTagIds', { id, followedTagIds: tags.map(tag => tag.name) })
|
||||||
|
commit('saveFollowedTagPagination', { id, pagination })
|
||||||
|
return tags
|
||||||
|
})
|
||||||
|
},
|
||||||
clearFriends ({ commit }, userId) {
|
clearFriends ({ commit }, userId) {
|
||||||
commit('clearFriends', userId)
|
commit('clearFriends', userId)
|
||||||
},
|
},
|
||||||
clearFollowers ({ commit }, userId) {
|
clearFollowers ({ commit }, userId) {
|
||||||
commit('clearFollowers', userId)
|
commit('clearFollowers', userId)
|
||||||
},
|
},
|
||||||
|
clearFollowedTags ({ commit }, userId) {
|
||||||
|
commit('clearFollowedTags', userId)
|
||||||
|
},
|
||||||
subscribeUser ({ rootState, commit }, id) {
|
subscribeUser ({ rootState, commit }, id) {
|
||||||
return rootState.api.backendInteractor.subscribeUser({ id })
|
return rootState.api.backendInteractor.subscribeUser({ id })
|
||||||
.then((relationship) => commit('updateUserRelationship', [relationship]))
|
.then((relationship) => commit('updateUserRelationship', [relationship]))
|
||||||
|
@ -473,6 +510,12 @@ const users = {
|
||||||
store.commit('setUserForNotification', notification)
|
store.commit('setUserForNotification', notification)
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
decrementFollowRequestsCount (store) {
|
||||||
|
store.commit('decrementFollowRequestsCount')
|
||||||
|
},
|
||||||
|
incrementFollowRequestsCount (store) {
|
||||||
|
store.commit('incrementFollowRequestsCount')
|
||||||
|
},
|
||||||
searchUsers ({ rootState, commit }, { query }) {
|
searchUsers ({ rootState, commit }, { query }) {
|
||||||
return rootState.api.backendInteractor.searchUsers({ query })
|
return rootState.api.backendInteractor.searchUsers({ query })
|
||||||
.then((users) => {
|
.then((users) => {
|
||||||
|
@ -536,7 +579,6 @@ const users = {
|
||||||
store.dispatch('stopFetchingTimeline', 'friends')
|
store.dispatch('stopFetchingTimeline', 'friends')
|
||||||
store.commit('setBackendInteractor', backendInteractorService(store.getters.getToken()))
|
store.commit('setBackendInteractor', backendInteractorService(store.getters.getToken()))
|
||||||
store.dispatch('stopFetchingNotifications')
|
store.dispatch('stopFetchingNotifications')
|
||||||
store.dispatch('stopFetchingFollowRequests')
|
|
||||||
store.dispatch('stopFetchingConfig')
|
store.dispatch('stopFetchingConfig')
|
||||||
store.commit('clearNotifications')
|
store.commit('clearNotifications')
|
||||||
store.commit('resetStatuses')
|
store.commit('resetStatuses')
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import { each, map, concat, last, get } from 'lodash'
|
import { each, map, concat, last, get } from 'lodash'
|
||||||
import { parseStatus, parseSource, parseUser, parseNotification, parseReport, parseAttachment, parseLinkHeaderPagination } from '../entity_normalizer/entity_normalizer.service.js'
|
import { parseStatus, parseSource, parseUser, parseNotification, parseReport, parseAttachment, parseLinkHeaderPagination } from '../entity_normalizer/entity_normalizer.service.js'
|
||||||
import { RegistrationError, StatusCodeError } from '../errors/errors'
|
import { RegistrationError, StatusCodeError } from '../errors/errors'
|
||||||
|
import { Url } from 'url'
|
||||||
|
|
||||||
/* eslint-env browser */
|
/* eslint-env browser */
|
||||||
const MUTES_IMPORT_URL = '/api/pleroma/mutes_import'
|
const MUTES_IMPORT_URL = '/api/pleroma/mutes_import'
|
||||||
|
@ -11,11 +12,11 @@ const CHANGE_EMAIL_URL = '/api/pleroma/change_email'
|
||||||
const CHANGE_PASSWORD_URL = '/api/pleroma/change_password'
|
const CHANGE_PASSWORD_URL = '/api/pleroma/change_password'
|
||||||
const MOVE_ACCOUNT_URL = '/api/pleroma/move_account'
|
const MOVE_ACCOUNT_URL = '/api/pleroma/move_account'
|
||||||
const ALIASES_URL = '/api/pleroma/aliases'
|
const ALIASES_URL = '/api/pleroma/aliases'
|
||||||
const TAG_USER_URL = '/api/pleroma/admin/users/tag'
|
const TAG_USER_URL = '/api/v1/pleroma/admin/users/tag'
|
||||||
const PERMISSION_GROUP_URL = (screenName, right) => `/api/pleroma/admin/users/${screenName}/permission_group/${right}`
|
const PERMISSION_GROUP_URL = (screenName, right) => `/api/v1/pleroma/admin/users/${screenName}/permission_group/${right}`
|
||||||
const ACTIVATE_USER_URL = '/api/pleroma/admin/users/activate'
|
const ACTIVATE_USER_URL = '/api/v1/pleroma/admin/users/activate'
|
||||||
const DEACTIVATE_USER_URL = '/api/pleroma/admin/users/deactivate'
|
const DEACTIVATE_USER_URL = '/api/v1/pleroma/admin/users/deactivate'
|
||||||
const ADMIN_USERS_URL = '/api/pleroma/admin/users'
|
const ADMIN_USERS_URL = '/api/v1/pleroma/admin/users'
|
||||||
const SUGGESTIONS_URL = '/api/v1/suggestions'
|
const SUGGESTIONS_URL = '/api/v1/suggestions'
|
||||||
const NOTIFICATION_SETTINGS_URL = '/api/pleroma/notification_settings'
|
const NOTIFICATION_SETTINGS_URL = '/api/pleroma/notification_settings'
|
||||||
const NOTIFICATION_READ_URL = '/api/v1/pleroma/notifications/read'
|
const NOTIFICATION_READ_URL = '/api/v1/pleroma/notifications/read'
|
||||||
|
@ -111,6 +112,7 @@ const AKKOMA_SETTING_PROFILE_LIST = `/api/v1/akkoma/frontend_settings/pleroma-fe
|
||||||
const MASTODON_TAG_URL = (name) => `/api/v1/tags/${name}`
|
const MASTODON_TAG_URL = (name) => `/api/v1/tags/${name}`
|
||||||
const MASTODON_FOLLOW_TAG_URL = (name) => `/api/v1/tags/${name}/follow`
|
const MASTODON_FOLLOW_TAG_URL = (name) => `/api/v1/tags/${name}/follow`
|
||||||
const MASTODON_UNFOLLOW_TAG_URL = (name) => `/api/v1/tags/${name}/unfollow`
|
const MASTODON_UNFOLLOW_TAG_URL = (name) => `/api/v1/tags/${name}/unfollow`
|
||||||
|
const MASTODON_FOLLOWED_TAGS_URL = '/api/v1/followed_tags'
|
||||||
|
|
||||||
const oldfetch = window.fetch
|
const oldfetch = window.fetch
|
||||||
|
|
||||||
|
@ -246,7 +248,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) => {
|
const authHeaders = (accessToken) => {
|
||||||
if (accessToken) {
|
if (accessToken) {
|
||||||
|
@ -404,14 +406,6 @@ const fetchFollowers = ({ id, maxId, sinceId, limit = 20, credentials }) => {
|
||||||
.then((data) => data.json())
|
.then((data) => data.json())
|
||||||
.then((data) => data.map(parseUser))
|
.then((data) => data.map(parseUser))
|
||||||
}
|
}
|
||||||
|
|
||||||
const fetchFollowRequests = ({ credentials }) => {
|
|
||||||
const url = MASTODON_FOLLOW_REQUESTS_URL
|
|
||||||
return fetch(url, { headers: authHeaders(credentials) })
|
|
||||||
.then((data) => data.json())
|
|
||||||
.then((data) => data.map(parseUser))
|
|
||||||
}
|
|
||||||
|
|
||||||
const fetchLists = ({ credentials }) => {
|
const fetchLists = ({ credentials }) => {
|
||||||
const url = MASTODON_LISTS_URL
|
const url = MASTODON_LISTS_URL
|
||||||
return fetch(url, { headers: authHeaders(credentials) })
|
return fetch(url, { headers: authHeaders(credentials) })
|
||||||
|
@ -878,7 +872,8 @@ const postStatus = ({
|
||||||
quoteId,
|
quoteId,
|
||||||
contentType,
|
contentType,
|
||||||
preview,
|
preview,
|
||||||
idempotencyKey
|
idempotencyKey,
|
||||||
|
language
|
||||||
}) => {
|
}) => {
|
||||||
const form = new FormData()
|
const form = new FormData()
|
||||||
const pollOptions = poll.options || []
|
const pollOptions = poll.options || []
|
||||||
|
@ -889,6 +884,7 @@ const postStatus = ({
|
||||||
if (visibility) form.append('visibility', visibility)
|
if (visibility) form.append('visibility', visibility)
|
||||||
if (sensitive) form.append('sensitive', sensitive)
|
if (sensitive) form.append('sensitive', sensitive)
|
||||||
if (contentType) form.append('content_type', contentType)
|
if (contentType) form.append('content_type', contentType)
|
||||||
|
if (language) form.append('language', language)
|
||||||
mediaIds.forEach(val => {
|
mediaIds.forEach(val => {
|
||||||
form.append('media_ids[]', val)
|
form.append('media_ids[]', val)
|
||||||
})
|
})
|
||||||
|
@ -1335,7 +1331,7 @@ const fetchEmojiReactions = ({ id, credentials }) => {
|
||||||
|
|
||||||
const reactWithEmoji = ({ id, emoji, credentials }) => {
|
const reactWithEmoji = ({ id, emoji, credentials }) => {
|
||||||
return promisedRequest({
|
return promisedRequest({
|
||||||
url: PLEROMA_EMOJI_REACT_URL(id, emoji),
|
url: PLEROMA_EMOJI_REACT_URL(id, encodeURIComponent(emoji)),
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
credentials
|
credentials
|
||||||
}).then(parseStatus)
|
}).then(parseStatus)
|
||||||
|
@ -1343,7 +1339,7 @@ const reactWithEmoji = ({ id, emoji, credentials }) => {
|
||||||
|
|
||||||
const unreactWithEmoji = ({ id, emoji, credentials }) => {
|
const unreactWithEmoji = ({ id, emoji, credentials }) => {
|
||||||
return promisedRequest({
|
return promisedRequest({
|
||||||
url: PLEROMA_EMOJI_UNREACT_URL(id, emoji),
|
url: PLEROMA_EMOJI_UNREACT_URL(id, encodeURIComponent(emoji)),
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
credentials
|
credentials
|
||||||
}).then(parseStatus)
|
}).then(parseStatus)
|
||||||
|
@ -1575,6 +1571,48 @@ const unfollowHashtag = ({ tag, credentials }) => {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getFollowedHashtags = ({ credentials, pagination: savedPagination }) => {
|
||||||
|
const queryParams = new URLSearchParams()
|
||||||
|
if (savedPagination?.maxId) {
|
||||||
|
queryParams.append('max_id', savedPagination.maxId)
|
||||||
|
}
|
||||||
|
const url = `${MASTODON_FOLLOWED_TAGS_URL}?${queryParams.toString()}`
|
||||||
|
let pagination = {};
|
||||||
|
return fetch(url, {
|
||||||
|
credentials
|
||||||
|
}).then((data) => {
|
||||||
|
pagination = parseLinkHeaderPagination(data.headers.get('Link'), {
|
||||||
|
flakeId: false
|
||||||
|
});
|
||||||
|
return data.json()
|
||||||
|
}).then((data) => {
|
||||||
|
return {
|
||||||
|
pagination,
|
||||||
|
data
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const getFollowRequests = ({ credentials, pagination: savedPagination }) => {
|
||||||
|
const queryParams = new URLSearchParams()
|
||||||
|
if (savedPagination?.maxId) {
|
||||||
|
queryParams.append('max_id', savedPagination.maxId)
|
||||||
|
}
|
||||||
|
const url = `${MASTODON_FOLLOW_REQUESTS_URL}?${queryParams.toString()}`
|
||||||
|
let pagination = {};
|
||||||
|
return fetch(url, {
|
||||||
|
credentials
|
||||||
|
}).then((data) => {
|
||||||
|
pagination = parseLinkHeaderPagination(data.headers.get('Link'), { flakeId: true });
|
||||||
|
return data.json()
|
||||||
|
}).then((data) => {
|
||||||
|
return {
|
||||||
|
pagination,
|
||||||
|
data: data.map(parseUser)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export const getMastodonSocketURI = ({ credentials, stream, args = {} }) => {
|
export const getMastodonSocketURI = ({ credentials, stream, args = {} }) => {
|
||||||
return Object.entries({
|
return Object.entries({
|
||||||
...(credentials
|
...(credentials
|
||||||
|
@ -1764,7 +1802,6 @@ const apiService = {
|
||||||
mfaConfirmOTP,
|
mfaConfirmOTP,
|
||||||
addBackup,
|
addBackup,
|
||||||
listBackups,
|
listBackups,
|
||||||
fetchFollowRequests,
|
|
||||||
fetchLists,
|
fetchLists,
|
||||||
createList,
|
createList,
|
||||||
getList,
|
getList,
|
||||||
|
@ -1813,7 +1850,9 @@ const apiService = {
|
||||||
deleteNoteFromReport,
|
deleteNoteFromReport,
|
||||||
getHashtag,
|
getHashtag,
|
||||||
followHashtag,
|
followHashtag,
|
||||||
unfollowHashtag
|
unfollowHashtag,
|
||||||
|
getFollowedHashtags,
|
||||||
|
getFollowRequests
|
||||||
}
|
}
|
||||||
|
|
||||||
export default apiService
|
export default apiService
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
import apiService, { getMastodonSocketURI, ProcessedWS } from '../api/api.service.js'
|
import apiService, { getMastodonSocketURI, ProcessedWS } from '../api/api.service.js'
|
||||||
import timelineFetcher from '../timeline_fetcher/timeline_fetcher.service.js'
|
import timelineFetcher from '../timeline_fetcher/timeline_fetcher.service.js'
|
||||||
import notificationsFetcher from '../notifications_fetcher/notifications_fetcher.service.js'
|
import notificationsFetcher from '../notifications_fetcher/notifications_fetcher.service.js'
|
||||||
import followRequestFetcher from '../../services/follow_request_fetcher/follow_request_fetcher.service'
|
|
||||||
import listsFetcher from '../../services/lists_fetcher/lists_fetcher.service.js'
|
import listsFetcher from '../../services/lists_fetcher/lists_fetcher.service.js'
|
||||||
import announcementsFetcher from '../../services/announcements_fetcher/announcements_fetcher.service.js'
|
import announcementsFetcher from '../../services/announcements_fetcher/announcements_fetcher.service.js'
|
||||||
import configFetcher from '../config_fetcher/config_fetcher.service.js'
|
import configFetcher from '../config_fetcher/config_fetcher.service.js'
|
||||||
|
@ -28,10 +27,6 @@ const backendInteractorService = credentials => ({
|
||||||
return notificationsFetcher.fetchAndUpdate({ ...args, credentials })
|
return notificationsFetcher.fetchAndUpdate({ ...args, credentials })
|
||||||
},
|
},
|
||||||
|
|
||||||
startFetchingFollowRequests ({ store }) {
|
|
||||||
return followRequestFetcher.startFetching({ store, credentials })
|
|
||||||
},
|
|
||||||
|
|
||||||
startFetchingLists ({ store }) {
|
startFetchingLists ({ store }) {
|
||||||
return listsFetcher.startFetching({ store, credentials })
|
return listsFetcher.startFetching({ store, credentials })
|
||||||
},
|
},
|
||||||
|
|
|
@ -68,13 +68,15 @@ export const parseUser = (data) => {
|
||||||
output.fields_html = data.fields.map(field => {
|
output.fields_html = data.fields.map(field => {
|
||||||
return {
|
return {
|
||||||
name: escape(field.name),
|
name: escape(field.name),
|
||||||
value: field.value
|
value: field.value,
|
||||||
|
verified_at: field.verified_at
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
output.fields_text = data.fields.map(field => {
|
output.fields_text = data.fields.map(field => {
|
||||||
return {
|
return {
|
||||||
name: unescape(field.name.replace(/<[^>]*>/g, '')),
|
name: unescape(field.name.replace(/<[^>]*>/g, '')),
|
||||||
value: unescape(field.value.replace(/<[^>]*>/g, ''))
|
value: unescape(field.value.replace(/<[^>]*>/g, '')),
|
||||||
|
verified_at: field.verified_at
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -88,6 +90,7 @@ export const parseUser = (data) => {
|
||||||
output.friends_count = data.following_count
|
output.friends_count = data.following_count
|
||||||
|
|
||||||
output.bot = data.bot
|
output.bot = data.bot
|
||||||
|
output.follow_requests_count = data.follow_requests_count
|
||||||
if (data.akkoma) {
|
if (data.akkoma) {
|
||||||
output.instance = data.akkoma.instance
|
output.instance = data.akkoma.instance
|
||||||
output.status_ttl_days = data.akkoma.status_ttl_days
|
output.status_ttl_days = data.akkoma.status_ttl_days
|
||||||
|
@ -232,13 +235,14 @@ export const parseAttachment = (data) => {
|
||||||
if (masto) {
|
if (masto) {
|
||||||
// Not exactly same...
|
// Not exactly same...
|
||||||
output.mimetype = data.pleroma ? data.pleroma.mime_type : data.type
|
output.mimetype = data.pleroma ? data.pleroma.mime_type : data.type
|
||||||
output.meta = data.meta // not present in BE yet
|
output.meta = data.meta
|
||||||
output.id = data.id
|
output.id = data.id
|
||||||
} else {
|
} else {
|
||||||
output.mimetype = data.mimetype
|
output.mimetype = data.mimetype
|
||||||
// output.meta = ??? missing
|
// output.meta = ??? missing
|
||||||
}
|
}
|
||||||
|
|
||||||
|
output.blurhash = data.blurhash
|
||||||
output.url = data.url
|
output.url = data.url
|
||||||
output.large_thumb_url = data.preview_url
|
output.large_thumb_url = data.preview_url
|
||||||
output.description = data.description
|
output.description = data.description
|
||||||
|
@ -408,8 +412,10 @@ export const parseNotification = (data) => {
|
||||||
if (masto) {
|
if (masto) {
|
||||||
output.type = mastoDict[data.type] || data.type
|
output.type = mastoDict[data.type] || data.type
|
||||||
output.seen = data.pleroma.is_seen
|
output.seen = data.pleroma.is_seen
|
||||||
output.status = isStatusNotification(output.type) ? parseStatus(data.status) : null
|
if (data.status) {
|
||||||
output.action = output.status // TODO: Refactor, this is unneeded
|
output.status = isStatusNotification(output.type) ? parseStatus(data.status) : null
|
||||||
|
output.action = output.status // TODO: Refactor, this is unneeded
|
||||||
|
}
|
||||||
output.target = output.type !== 'move'
|
output.target = output.type !== 'move'
|
||||||
? null
|
? null
|
||||||
: parseUser(data.target)
|
: parseUser(data.target)
|
||||||
|
|
|
@ -1,23 +0,0 @@
|
||||||
import apiService from '../api/api.service.js'
|
|
||||||
import { promiseInterval } from '../promise_interval/promise_interval.js'
|
|
||||||
|
|
||||||
const fetchAndUpdate = ({ store, credentials }) => {
|
|
||||||
return apiService.fetchFollowRequests({ credentials })
|
|
||||||
.then((requests) => {
|
|
||||||
store.commit('setFollowRequests', requests)
|
|
||||||
store.commit('addNewUsers', requests)
|
|
||||||
}, () => {})
|
|
||||||
.catch(() => {})
|
|
||||||
}
|
|
||||||
|
|
||||||
const startFetching = ({ credentials, store }) => {
|
|
||||||
const boundFetchAndUpdate = () => fetchAndUpdate({ credentials, store })
|
|
||||||
boundFetchAndUpdate()
|
|
||||||
return promiseInterval(boundFetchAndUpdate, 240000)
|
|
||||||
}
|
|
||||||
|
|
||||||
const followRequestFetcher = {
|
|
||||||
startFetching
|
|
||||||
}
|
|
||||||
|
|
||||||
export default followRequestFetcher
|
|
|
@ -13,7 +13,8 @@ const postStatus = ({
|
||||||
quoteId = undefined,
|
quoteId = undefined,
|
||||||
contentType = 'text/plain',
|
contentType = 'text/plain',
|
||||||
preview = false,
|
preview = false,
|
||||||
idempotencyKey = ''
|
idempotencyKey = '',
|
||||||
|
language
|
||||||
}) => {
|
}) => {
|
||||||
const mediaIds = map(media, 'id')
|
const mediaIds = map(media, 'id')
|
||||||
|
|
||||||
|
@ -29,7 +30,8 @@ const postStatus = ({
|
||||||
contentType,
|
contentType,
|
||||||
poll,
|
poll,
|
||||||
preview,
|
preview,
|
||||||
idempotencyKey
|
idempotencyKey,
|
||||||
|
language
|
||||||
})
|
})
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
if (!data.error && !preview) {
|
if (!data.error && !preview) {
|
||||||
|
|
|
@ -4,12 +4,10 @@ import { getColors, computeDynamicColor, getOpacitySlot } from '../theme_data/th
|
||||||
|
|
||||||
export const applyTheme = (input) => {
|
export const applyTheme = (input) => {
|
||||||
const { rules } = generatePreset(input)
|
const { rules } = generatePreset(input)
|
||||||
const head = document.head
|
|
||||||
const body = document.body
|
const body = document.body
|
||||||
body.classList.add('hidden')
|
body.classList.add('hidden')
|
||||||
|
|
||||||
const styleEl = document.createElement('style')
|
const styleEl = document.getElementById('theme-holder')
|
||||||
head.appendChild(styleEl)
|
|
||||||
const styleSheet = styleEl.sheet
|
const styleSheet = styleEl.sheet
|
||||||
|
|
||||||
styleSheet.toString()
|
styleSheet.toString()
|
||||||
|
|
45
static/.tos
45
static/.tos
|
@ -1,45 +0,0 @@
|
||||||
<h4>Terms of Service</h4>
|
|
||||||
|
|
||||||
<p>It's mainly "be nice"</p>
|
|
||||||
|
|
||||||
<ol>
|
|
||||||
<li>
|
|
||||||
<h3>Don't be a big meanie</h4>
|
|
||||||
<p>Arguments are cool and all but don't make them into flamewars. Try to act in good faith - we want to be at least on good terms with people. Please act with understanding towards others on this instance. Most people here are probably struggling with a lot, be mindful of that.</p>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<h3>Mark your lewds!</h3>
|
|
||||||
<p>Reminder that lewd is bad and nobody wants to be forced to see that. Just mark it sensitive, and post unlisted. That is to say, anything suggestive/ecchi upwards should be marked. If you wouldn't look at it with your parents/boss in the room, mark it. It goes without saying that if you're <em>going</em> to post lewd stuff, keep it sensible. Obviously nothing underaged or otherwise questionable. Or you could just not post lewd stuff. Either/or.</p>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<h3>This is a <b>Kink Shame Zone</b></h3>
|
|
||||||
<p>Being a lewdie will be met with many anime girl reaction images shaming you for your lewdness. Go think about icky things on someone else's webzone™</p>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<h3>Keep it legal!</h3>
|
|
||||||
<p>Server is hosted in france, keep content legal for there (+ wherever you're browsing from)</p>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<h3>No ads/spambots</h3>
|
|
||||||
<p>I didn't think I'd have to specify this, but please do not set up bots solely for trying to advertise.</h3>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<h3>Non-TOS recommendations</h3>
|
|
||||||
<p>This is stuff that'd I'd <em>like</em> you to do, but I won't outright ban you if you don't follow them</p>
|
|
||||||
<ul>
|
|
||||||
<li>If someone is sadposting, don't antagonise them - they probably just want to vent</li>
|
|
||||||
<li>Put walls of text behind a subject (CW) - helps the timeline not get flooded with text</li>
|
|
||||||
</ul>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li>
|
|
||||||
<h3>Other</h3>
|
|
||||||
<p>If you're here and you happen to play minecraft, feel free to message me with your username and come play with us sometime!</p>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
</ol>
|
|
||||||
|
|
||||||
<p>So I guess yeah, that's about it. Try to be nice, eh? We're probably all sad here.</p>
|
|
||||||
|
|
||||||
<br>
|
|
||||||
<img src="/static/logo.png" style="display: block; margin: auto; max-width: 100%; height: 50px; object-fit: contain;" />
|
|
BIN
static/blurhash-overlay.png
Executable file
BIN
static/blurhash-overlay.png
Executable file
Binary file not shown.
After Width: | Height: | Size: 27 KiB |
BIN
static/logo.png
BIN
static/logo.png
Binary file not shown.
Before Width: | Height: | Size: 5.8 KiB |
1
static/theme-holder.css
Normal file
1
static/theme-holder.css
Normal file
|
@ -0,0 +1 @@
|
||||||
|
// This file intentionally left blank
|
23
yarn.lock
23
yarn.lock
|
@ -1350,6 +1350,13 @@
|
||||||
minimatch "^3.0.4"
|
minimatch "^3.0.4"
|
||||||
strip-json-comments "^3.1.1"
|
strip-json-comments "^3.1.1"
|
||||||
|
|
||||||
|
"@floatingghost/pinch-zoom-element@^1.3.1":
|
||||||
|
version "1.3.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/@floatingghost/pinch-zoom-element/-/pinch-zoom-element-1.3.1.tgz#5f327ad17ddf1f56777098aca088fdbf99cbd049"
|
||||||
|
integrity sha512-KnE7aBQdd/Fj1TzU5uzgwD9YAQ58DTMUks/PoTEBFW4zi0lBM9cN/j45wzcnzsT2VXG1S6qM7NMmq7NGm2//Fg==
|
||||||
|
dependencies:
|
||||||
|
pointer-tracker "^2.0.3"
|
||||||
|
|
||||||
"@fortawesome/fontawesome-common-types@6.2.0":
|
"@fortawesome/fontawesome-common-types@6.2.0":
|
||||||
version "6.2.0"
|
version "6.2.0"
|
||||||
resolved "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.2.0.tgz"
|
resolved "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.2.0.tgz"
|
||||||
|
@ -1516,13 +1523,6 @@
|
||||||
"@jridgewell/resolve-uri" "^3.0.3"
|
"@jridgewell/resolve-uri" "^3.0.3"
|
||||||
"@jridgewell/sourcemap-codec" "^1.4.10"
|
"@jridgewell/sourcemap-codec" "^1.4.10"
|
||||||
|
|
||||||
"@kazvmoe-infra/pinch-zoom-element@1.2.0":
|
|
||||||
version "1.2.0"
|
|
||||||
resolved "https://registry.npmjs.org/@kazvmoe-infra/pinch-zoom-element/-/pinch-zoom-element-1.2.0.tgz"
|
|
||||||
integrity sha512-HBrhH5O/Fsp2bB7EGTXzCsBAVcMjknSagKC5pBdGpKsF8meHISR0kjDIdw4YoE0S+0oNMwJ6ZUZyIBrdywxPPw==
|
|
||||||
dependencies:
|
|
||||||
pointer-tracker "^2.0.3"
|
|
||||||
|
|
||||||
"@nicolo-ribaudo/eslint-scope-5-internals@5.1.1-v1":
|
"@nicolo-ribaudo/eslint-scope-5-internals@5.1.1-v1":
|
||||||
version "5.1.1-v1"
|
version "5.1.1-v1"
|
||||||
resolved "https://registry.yarnpkg.com/@nicolo-ribaudo/eslint-scope-5-internals/-/eslint-scope-5-internals-5.1.1-v1.tgz#dbf733a965ca47b1973177dc0bb6c889edcfb129"
|
resolved "https://registry.yarnpkg.com/@nicolo-ribaudo/eslint-scope-5-internals/-/eslint-scope-5-internals-5.1.1-v1.tgz#dbf733a965ca47b1973177dc0bb6c889edcfb129"
|
||||||
|
@ -2494,6 +2494,11 @@ binary-extensions@^2.0.0:
|
||||||
resolved "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz"
|
resolved "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz"
|
||||||
integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==
|
integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==
|
||||||
|
|
||||||
|
blurhash@^2.0.4:
|
||||||
|
version "2.0.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/blurhash/-/blurhash-2.0.4.tgz#60642a823b50acaaf3732ddb6c7dfd721bdfef2a"
|
||||||
|
integrity sha512-r/As72u2FbucLoK5NTegM/GucxJc3d8GvHc4ngo13IO/nt2HU4gONxNLq1XPN6EM/V8Y9URIa7PcSz2RZu553A==
|
||||||
|
|
||||||
body-parser@1.19.2:
|
body-parser@1.19.2:
|
||||||
version "1.19.2"
|
version "1.19.2"
|
||||||
resolved "https://registry.npmjs.org/body-parser/-/body-parser-1.19.2.tgz"
|
resolved "https://registry.npmjs.org/body-parser/-/body-parser-1.19.2.tgz"
|
||||||
|
@ -5090,9 +5095,9 @@ isexe@^2.0.0:
|
||||||
resolved "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz"
|
resolved "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz"
|
||||||
integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==
|
integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==
|
||||||
|
|
||||||
iso-639-1@2.1.15:
|
iso-639-1@^2.1.15:
|
||||||
version "2.1.15"
|
version "2.1.15"
|
||||||
resolved "https://registry.npmjs.org/iso-639-1/-/iso-639-1-2.1.15.tgz"
|
resolved "https://registry.yarnpkg.com/iso-639-1/-/iso-639-1-2.1.15.tgz#20cf78a4f691aeb802c16f17a6bad7d99271e85d"
|
||||||
integrity sha512-7c7mBznZu2ktfvyT582E2msM+Udc1EjOyhVRE/0ZsjD9LBtWSm23h3PtiRh2a35XoUsTQQjJXaJzuLjXsOdFDg==
|
integrity sha512-7c7mBznZu2ktfvyT582E2msM+Udc1EjOyhVRE/0ZsjD9LBtWSm23h3PtiRh2a35XoUsTQQjJXaJzuLjXsOdFDg==
|
||||||
|
|
||||||
isobject@^3.0.1:
|
isobject@^3.0.1:
|
||||||
|
|
Loading…
Reference in a new issue