Compare commits

...

42 Commits

Author SHA1 Message Date
Fristi fe07c22b1c Merge branch 'stable' into testing 2023-02-19 09:59:54 +01:00
FloatingGhost 8569b5946e Merge branch 'develop' into stable 2023-02-11 10:50:04 +00:00
floatingghost 6a9d169e24 Merge pull request 'components: emoji_reactions: force custom emoji reaction height' (#280) from a1batross/akkoma-fe:a1batross-patch-1 into develop
Reviewed-on: AkkomaGang/akkoma-fe#280
2023-02-11 10:41:00 +00:00
a1batross 581c53a15e components: emoji_reactions: force custom emoji reaction height
Prevents the usage of too long emoji reactions
2023-02-10 23:28:46 +00:00
yanchan09 9e04e4fd80 Improve emoji picker performance (#275)
A simple virtual scroller is now used for the emoji grid. This avoids loading all emoji images at once, saving network bandwidth and reducing load on the server, while also putting less work on the browser's DOM and layout engine.

Co-authored-by: yan <yan@omg.lol>
Reviewed-on: AkkomaGang/akkoma-fe#275
Co-authored-by: yanchan09 <yan@omg.lol>
Co-committed-by: yanchan09 <yan@omg.lol>
2023-02-04 21:10:06 +00:00
floatingghost 88d5149db5 paginate-follow-requests (#277)
Co-authored-by: FloatingGhost <hannah@coffee-and-dreams.uk>
Reviewed-on: AkkomaGang/akkoma-fe#277
2023-02-04 21:09:09 +00:00
floatingghost b4b13d777f Merge pull request 'Add indicator to user card if user blocks you' (#274) from eris/pleroma-fe:block-indicator into develop
Reviewed-on: AkkomaGang/akkoma-fe#274
2023-01-27 10:08:17 +00:00
eris 7f4dd9ff03 Disable follow button if blocked by user 2023-01-27 00:30:47 +00:00
eris a9a95e9120 Add indicator if user blocks you 2023-01-27 00:30:30 +00:00
FloatingGhost 56fd2e773b Merge branch 'languages' into develop 2023-01-15 17:59:50 +00:00
FloatingGhost 42dc1a027a add language input 2023-01-15 17:59:32 +00:00
floatingghost 236bc2c762 Merge pull request 'Only show "keep open" emoji checkbox on post form' (#269) from sfr/pleroma-fe:fix/keepopen into develop
Reviewed-on: AkkomaGang/akkoma-fe#269
2023-01-09 22:20:31 +00:00
Sol Fisher Romanoff e9f47509ae
Only show "keep open" emoji checkbox on post form 2023-01-03 16:04:26 +02:00
FloatingGhost f288d0c219 Make everything work with a strict CSP 2023-01-02 15:16:42 +00:00
FloatingGhost d973396c96 Remove console.log 2023-01-01 21:06:02 +00:00
FloatingGhost 62287fffae add follow/unfollow to followed tags list 2023-01-01 21:05:25 +00:00
FloatingGhost e9f16af82d Add list of followed hashtags to profile 2023-01-01 20:11:07 +00:00
floatingghost dfba8be134 Fall back to nsfw image if no blurhash 2022-12-30 05:03:25 +00:00
FloatingGhost 313ddcebcb Add blurhash support 2022-12-30 04:57:23 +00:00
FloatingGhost 236b19e854 Merge branch 'develop' of akkoma.dev:AkkomaGang/akkoma-fe into develop 2022-12-30 03:20:30 +00:00
FloatingGhost ea941d7cfa remove IHBA assets 2022-12-30 03:20:12 +00:00
Beefox 2e5001e5de Allow follow(er) lists to be acessible by account owner even if follower counts are disabled (#246)
Currently, if a user has their follower/follow counts hidden, they cannot access their own list of followers/follows. This makes no real sense and means that they cannot modify those lists without disabling their privacy options.

This fix simply allows those tabs to be accessed no matter if the counts are hidden or not.

Reviewed-on: AkkomaGang/akkoma-fe#246
Co-authored-by: Beefox <bee@beefox.xyz>
Co-committed-by: Beefox <bee@beefox.xyz>
2022-12-30 03:04:15 +00:00
Norm 014f8b0dd2 Make minimum width for 3-column layout 1280px (#255) (#256)
1280px is a pretty common screen width for several resolutions
(1280x720, 1280x800, 1280x1024, etc.). Since it is only 20px less than
the current 1300px minimum, this shouldn't be a big issue to lower the
minimum screen width for the 3-column layout to 1280px.

Closes: AkkomaGang/pleroma-fe#255

Co-authored-by: Francis Dinh <normandy@biribiri.dev>
Reviewed-on: AkkomaGang/akkoma-fe#256
Co-authored-by: Norm <normandy@biribiri.dev>
Co-committed-by: Norm <normandy@biribiri.dev>
2022-12-30 03:01:17 +00:00
floatingghost dd403b295f Merge pull request 'Remove stray debug log' (#265) from sfr/pleroma-fe:del-log into develop
Reviewed-on: AkkomaGang/akkoma-fe#265
2022-12-30 03:00:49 +00:00
Sol Fisher Romanoff 9cd62fe08d
Remove stray debug log 2022-12-30 01:03:31 +02:00
FloatingGhost f668455dff Merge branch 'link-verification' into develop 2022-12-29 20:56:22 +00:00
FloatingGhost 5a4315384e force CI build 2022-12-29 15:25:03 +00:00
FloatingGhost 401dfa8fa6 update readme 2022-12-29 15:22:06 +00:00
FloatingGhost bb243168b3 Revert "Merge pull request 'Don't show timeline links if disabled and logged out' (#250) from sfr/pleroma-fe:fix/hide-timelines into develop"
This reverts commit 0b5793c1e0, reversing
changes made to 72ef2e7454.
2022-12-29 15:18:13 +00:00
FloatingGhost da491f3278 add verification of links 2022-12-29 15:17:35 +00:00
FloatingGhost d00e28d5e9 fix emoji picker in replies in notifications 2022-12-22 05:43:01 +00:00
FloatingGhost 7ff17ab722 don't crash out if notification status is null 2022-12-20 13:20:13 +00:00
floatingghost b009428814 Merge pull request 'Revert "Revert "use v1 urls""' (#254) from v1-urls into develop
Reviewed-on: AkkomaGang/pleroma-fe#254
2022-12-14 12:09:03 +00:00
floatingghost 7bec96a1bf Merge pull request 'Fix user moderation dropdown clipping' (#249) from sfr/pleroma-fe:fix/dropdown into develop
Reviewed-on: AkkomaGang/pleroma-fe#249
2022-12-14 12:08:27 +00:00
floatingghost 0b5793c1e0 Merge pull request 'Don't show timeline links if disabled and logged out' (#250) from sfr/pleroma-fe:fix/hide-timelines into develop
Reviewed-on: AkkomaGang/pleroma-fe#250
2022-12-14 12:08:08 +00:00
floatingghost 72ef2e7454 Merge pull request 'Fix 404 when reacting with Keycap Number Sign' (#252) from fef/pleroma-fe:develop into develop
Reviewed-on: AkkomaGang/pleroma-fe#252
2022-12-14 12:07:27 +00:00
FloatingGhost c39332c1bf Revert "Revert "use v1 urls""
This reverts commit 8c6cf86de3.
2022-12-14 09:39:01 +00:00
FloatingGhost 8c6cf86de3 Revert "use v1 urls"
This reverts commit 909271c764.
2022-12-14 09:38:46 +00:00
FloatingGhost 909271c764 use v1 urls 2022-12-14 09:38:07 +00:00
anna 413acbc7dd
fix 404 when reacting with Keycap Number Sign
The Unicode sequence for the Keycap Number Sign
emoji starts with an ASCII "#" character, which
the browser's URL parser will interpret as a URI
fragment and truncate it before sending the
request to the backend.
2022-12-12 18:59:57 +01:00
Sol Fisher Romanoff 6e1ba218df
Don't show timeline links if disabled and logged out 2022-12-10 21:39:50 +02:00
Sol Fisher Romanoff 830e8fdb45
Fix user moderation dropdown clipping 2022-12-10 21:03:12 +02:00
51 changed files with 799 additions and 387 deletions

View File

@ -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)
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)
- Custom emoji reactions
# 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.
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
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
@ -52,4 +52,4 @@ Edit config.json for configuration.
### 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.

View File

@ -10,6 +10,7 @@
<link rel="stylesheet" href="/static/font/css/lato.css">
<link rel="stylesheet" href="/static/mfm.css">
<link rel="stylesheet" href="/static/custom.css">
<link rel="stylesheet" href="/static/theme-holder.css" id="theme-holder">
<!--server-generated-meta-->
<link rel="icon" type="image/png" href="/favicon.png">
<link rel="manifest" href="/manifest.json">

View File

@ -18,19 +18,21 @@
"dependencies": {
"@babel/runtime": "7.17.8",
"@chenfengyuan/vue-qrcode": "2.0.0",
"@floatingghost/pinch-zoom-element": "^1.3.1",
"@fortawesome/fontawesome-svg-core": "1.3.0",
"@fortawesome/free-regular-svg-icons": "^6.1.2",
"@fortawesome/free-solid-svg-icons": "^6.2.0",
"@fortawesome/vue-fontawesome": "3.0.1",
"@kazvmoe-infra/pinch-zoom-element": "1.2.0",
"@vuelidate/core": "^2.0.0",
"@vuelidate/validators": "^2.0.0",
"blurhash": "^2.0.4",
"body-scroll-lock": "2.7.1",
"chromatism": "3.0.0",
"click-outside-vue3": "4.0.1",
"cropperjs": "1.5.12",
"diff": "3.5.0",
"escape-html": "1.0.3",
"iso-639-1": "^2.1.15",
"js-cookie": "^3.0.1",
"localforage": "1.10.0",
"parse-link-header": "^2.0.0",
@ -82,7 +84,6 @@
"html-webpack-plugin": "^5.5.0",
"http-proxy-middleware": "0.21.0",
"inject-loader": "2.0.1",
"iso-639-1": "2.1.15",
"isparta-loader": "2.0.0",
"json-loader": "0.5.7",
"karma": "6.3.17",

View File

@ -1,6 +1,7 @@
// stylelint-disable rscss/class-format
@import './_variables.scss';
@import '@fortawesome/fontawesome-svg-core/styles.css';
@import '@floatingghost/pinch-zoom-element/dist/pinch-zoom.css';
:root {
--navbar-height: 3.5rem;
--post-line-height: 1.4;

View File

@ -4,6 +4,8 @@ import { createRouter, createWebHistory } from 'vue-router'
import vClickOutside from 'click-outside-vue3'
import { FontAwesomeIcon, FontAwesomeLayers } from '@fortawesome/vue-fontawesome'
import { config } from '@fortawesome/fontawesome-svg-core';
config.autoAddCss = false
import App from '../App.vue'
import routes from './routes'

View File

@ -18,6 +18,7 @@ import {
faPencilAlt,
faAlignRight
} from '@fortawesome/free-solid-svg-icons'
import Blurhash from '../blurhash/Blurhash.vue'
library.add(
faFile,
@ -63,7 +64,8 @@ const Attachment = {
components: {
Flash,
StillImage,
VideoAttachment
VideoAttachment,
Blurhash
},
computed: {
classNames () {
@ -84,6 +86,9 @@ const Attachment = {
useContainFit () {
return this.$store.getters.mergedConfig.useContainFit
},
useBlurhash () {
return this.$store.getters.mergedConfig.useBlurhash
},
placeholderName () {
if (this.attachment.description === '' || !this.attachment.description) {
return this.type.toUpperCase()

View File

@ -64,7 +64,15 @@
:title="attachment.description"
@click.prevent.stop="toggleHidden"
>
<Blurhash
v-if="useBlurhash && attachment.blurhash"
:height="512"
:width="1024"
:hash="attachment.blurhash"
:punch="1"
/>
<img
v-else
:key="nsfwImage"
class="nsfw"
:src="nsfwImage"

View 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>

View File

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

View File

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

View File

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

View File

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

View File

@ -18,6 +18,7 @@
<EmojiPicker
v-if="enableEmojiPicker"
ref="picker"
show-keep-open
:class="{ hide: !showPicker }"
:enable-sticker-picker="enableStickerPicker"
class="emoji-picker-panel"

View File

@ -1,5 +1,6 @@
import { defineAsyncComponent } from 'vue'
import Checkbox from '../checkbox/checkbox.vue'
import EmojiGrid from '../emoji_grid/emoji_grid.vue'
import { library } from '@fortawesome/fontawesome-svg-core'
import {
faBoxOpen,
@ -14,19 +15,17 @@ library.add(
faSmileBeam
)
// At widest, approximately 20 emoji are visible in a row,
// loading 3 rows, could be overkill for narrow picker
const LOAD_EMOJI_BY = 60
// When to start loading new batch emoji, in pixels
const LOAD_EMOJI_MARGIN = 64
const EmojiPicker = {
props: {
enableStickerPicker: {
required: false,
type: Boolean,
default: false
},
showKeepOpen: {
required: false,
type: Boolean,
default: false
}
},
data () {
@ -34,16 +33,13 @@ const EmojiPicker = {
keyword: '',
activeGroup: 'standard',
showingStickers: false,
groupsScrolledClass: 'scrolled-top',
keepOpen: false,
customEmojiBufferSlice: LOAD_EMOJI_BY,
customEmojiTimeout: null,
customEmojiLoadAllConfirmed: false
keepOpen: false
}
},
components: {
StickerPicker: defineAsyncComponent(() => import('../sticker_picker/sticker_picker.vue')),
Checkbox
Checkbox,
EmojiGrid
},
methods: {
onStickerUploaded (e) {
@ -56,12 +52,6 @@ const EmojiPicker = {
const value = emoji.imageUrl ? `:${emoji.displayText}:` : emoji.replacement
this.$emit('emoji', { insertion: value, keepOpen: this.keepOpen })
},
onScroll (e) {
const target = (e && e.target) || this.$refs['emoji-groups']
this.updateScrolledClass(target)
this.scrolledGroup(target)
this.triggerLoadMore(target)
},
onWheel (e) {
e.preventDefault()
this.$refs['emoji-tabs'].scrollBy(e.deltaY, 0)
@ -69,68 +59,12 @@ const EmojiPicker = {
highlight (key) {
this.setShowStickers(false)
this.activeGroup = key
},
updateScrolledClass (target) {
if (target.scrollTop <= 5) {
this.groupsScrolledClass = 'scrolled-top'
} else if (target.scrollTop >= target.scrollTopMax - 5) {
this.groupsScrolledClass = 'scrolled-bottom'
} else {
this.groupsScrolledClass = 'scrolled-middle'
if (this.keyword.length) {
this.$refs.emojiGrid.scrollToItem(key)
}
},
triggerLoadMore (target) {
const ref = this.$refs['group-end-custom']
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
onActiveGroup (group) {
this.activeGroup = group
},
toggleStickers () {
this.showingStickers = !this.showingStickers
@ -146,13 +80,6 @@ const EmojiPicker = {
})
}
},
watch: {
keyword () {
this.customEmojiLoadAllConfirmed = false
this.onScroll()
this.startEmojiLoad(true)
}
},
computed: {
activeGroupView () {
return this.showingStickers ? '' : this.activeGroup
@ -168,9 +95,6 @@ const EmojiPicker = {
this.$store.state.instance.customEmoji || []
)
},
customEmojiBuffer () {
return this.filteredEmoji.slice(0, this.customEmojiBufferSlice)
},
emojis () {
const standardEmojis = this.$store.state.instance.emoji || []
const customEmojis = this.sortedEmoji

View File

@ -1,5 +1,16 @@
@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 {
.emoji-picker {
min-width: 160%;
@ -7,7 +18,7 @@
overflow: hidden;
left: -70%;
max-width: 100%;
@media (min-width: 800px) and (max-width: 1300px) {
@media (min-width: 800px) and (max-width: 1280px) {
left: -50%;
min-width: 50%;
max-width: 130%;
@ -18,6 +29,10 @@
min-width: 50%;
max-width: 130%;
}
.Status > .emoji-picker {
z-index: 1000;
}
}
}
.emoji-picker {
@ -70,10 +85,6 @@
flex-grow: 1;
}
.emoji-groups {
min-height: 200px;
}
.additional-tabs {
border-left: 1px solid;
border-left-color: $fallback--icon;
@ -152,76 +163,12 @@
}
}
.emoji {
&-search {
padding: 5px;
flex: 0 0 auto;
.emoji-search {
padding: 5px;
flex: 0 0 auto;
input {
width: 100%;
}
input {
width: 100%;
}
&-groups {
flex: 1 1 1px;
position: relative;
overflow: auto;
user-select: none;
mask: linear-gradient(to top, white 0, transparent 100%) bottom no-repeat,
linear-gradient(to bottom, white 0, transparent 100%) top no-repeat,
linear-gradient(to top, white, white);
transition: mask-size 150ms;
mask-size: 100% 20px, 100% 20px, auto;
// Autoprefixed seem to ignore this one, and also syntax is different
-webkit-mask-composite: xor;
mask-composite: exclude;
&.scrolled {
&-top {
mask-size: 100% 20px, 100% 0, auto;
}
&-bottom {
mask-size: 100% 0, 100% 20px, auto;
}
}
}
&-group {
display: flex;
align-items: center;
flex-wrap: wrap;
padding-left: 5px;
justify-content: left;
&-title {
font-size: 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%;
}
}
}
}

View File

@ -2,9 +2,9 @@
<div class="emoji-picker panel panel-default panel-body">
<div class="heading">
<span
ref="emoji-tabs"
class="emoji-tabs"
@wheel="onWheel"
ref="emoji-tabs"
>
<span
v-for="group in emojis"
@ -51,40 +51,16 @@
@input="$event.target.composing = false"
>
</div>
<EmojiGrid
ref="emojiGrid"
:groups="emojisView"
@emoji="onEmoji"
@active-group="onActiveGroup"
/>
<div
ref="emoji-groups"
class="emoji-groups"
:class="groupsScrolledClass"
@scroll="onScroll"
v-if="showKeepOpen"
class="keep-open"
>
<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">
{{ $t('emoji.keep_open') }}
</Checkbox>

View File

@ -19,6 +19,7 @@
:title="reaction.name"
class="reaction-emoji"
width="2.55em"
height="2.55em"
>
{{ reaction.count }}
</span>
@ -65,6 +66,7 @@
box-sizing: border-box;
.reaction-emoji {
width: 2.55em !important;
height: 2.55em !important;
margin-right: 0.25em;
}
&:focus {

View File

@ -144,6 +144,7 @@ const ExtraButtons = {
statusPoll: this.status.poll,
statusFiles: [...this.status.attachments],
statusScope: this.status.visibility,
statusLanguage: this.status.language,
statusContentType: data.content_type
}))
this.doDeleteStatus()

View File

@ -43,6 +43,7 @@ const FollowRequestCard = {
doApprove () {
this.$store.state.api.backendInteractor.approveUser({ id: this.user.id })
this.$store.dispatch('removeFollowRequest', this.user)
this.$store.dispatch('decrementFollowRequestsCount')
const notifId = this.findFollowRequestNotificationId()
this.$store.dispatch('markSingleNotificationAsSeen', { id: notifId })
@ -66,6 +67,7 @@ const FollowRequestCard = {
this.$store.state.api.backendInteractor.denyUser({ id: this.user.id })
.then(() => {
this.$store.dispatch('dismissNotificationLocal', { id: notifId })
this.$store.dispatch('decrementFollowRequestsCount')
this.$store.dispatch('removeFollowRequest', this.user)
})
this.hideDenyConfirmDialog()
@ -80,6 +82,11 @@ const FollowRequestCard = {
},
shouldConfirmDeny () {
return this.mergedConfig.modalOnDenyFollow
},
show () {
const notifId = this.$store.state.api.followRequests.find(req => req.id === this.user.id)
return notifId !== undefined
}
}
}

View File

@ -1,5 +1,5 @@
<template>
<basic-user-card :user="user">
<basic-user-card :user="user" v-if="show">
<div class="follow-request-card-content-container">
<button
class="btn button-default"

View File

@ -1,10 +1,26 @@
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 = {
components: {
FollowRequestCard
FollowRequestCard,
FollowRequestList
},
computed: {
userId () {
return this.$store.state.users.currentUser.id
},
requests () {
return this.$store.state.api.followRequests
}

View File

@ -6,12 +6,11 @@
</div>
</div>
<div class="panel-body">
<FollowRequestCard
v-for="request in requests"
:key="request.id"
:user="request"
class="list-item"
/>
<FollowRequestList :user-id="userId">
<template #item="{item}">
<FollowRequestCard :user="item" />
</template>
</FollowRequestList>
</div>
</div>
</template>

View 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>

View File

@ -33,11 +33,6 @@ library.add(
)
const NavPanel = {
created () {
if (this.currentUser && this.currentUser.locked) {
this.$store.dispatch('startFetchingFollowRequests')
}
},
components: {
TimelineMenuContent
},
@ -54,11 +49,13 @@ const NavPanel = {
computed: {
...mapState({
currentUser: state => state.users.currentUser,
followRequestCount: state => state.api.followRequests.length,
privateMode: state => state.instance.private,
federating: state => state.instance.federating
}),
...mapGetters(['unreadAnnouncementCount'])
...mapGetters(['unreadAnnouncementCount']),
followRequestCount () {
return this.$store.state.users.currentUser.follow_requests_count
}
}
}

View File

@ -1,4 +1,4 @@
import PinchZoom from '@kazvmoe-infra/pinch-zoom-element'
import PinchZoom from '@floatingghost/pinch-zoom-element'
export default {
methods: {

View File

@ -13,6 +13,7 @@ import suggestor from '../emoji_input/suggestor.js'
import { mapGetters, mapState } from 'vuex'
import Checkbox from '../checkbox/checkbox.vue'
import Select from '../select/select.vue'
import iso6391 from 'iso-639-1'
import { library } from '@fortawesome/fontawesome-svg-core'
import {
@ -63,6 +64,7 @@ const PostStatusForm = {
'statusMediaDescriptions',
'statusScope',
'statusContentType',
'statusLanguage',
'replyTo',
'quoteId',
'repliedUser',
@ -128,7 +130,7 @@ const PostStatusForm = {
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 = {
spoilerText: this.subject || '',
@ -139,6 +141,7 @@ const PostStatusForm = {
poll: {},
mediaDescriptions: {},
visibility: this.suggestedVisibility(),
language: interfaceLanguage,
contentType
}
@ -153,6 +156,7 @@ const PostStatusForm = {
poll: this.statusPoll || {},
mediaDescriptions: this.statusMediaDescriptions || {},
visibility: this.statusScope || this.suggestedVisibility(),
language: this.statusLanguage || interfaceLanguage,
contentType: statusContentType
}
}
@ -259,7 +263,10 @@ const PostStatusForm = {
...mapGetters(['mergedConfig']),
...mapState({
mobileLayout: state => state.interface.mobileLayout
})
}),
isoLanguages () {
return iso6391.getAllCodes();
}
},
watch: {
'newStatus': {
@ -282,6 +289,7 @@ const PostStatusForm = {
files: [],
visibility: newStatus.visibility,
contentType: newStatus.contentType,
language: newStatus.language,
poll: {},
mediaDescriptions: {}
}
@ -341,6 +349,7 @@ const PostStatusForm = {
inReplyToStatusId: this.replyTo,
quoteId: this.quoteId,
contentType: newStatus.contentType,
language: newStatus.language,
poll,
idempotencyKey: this.idempotencyKey
}
@ -375,6 +384,7 @@ const PostStatusForm = {
inReplyToStatusId: this.replyTo,
quoteId: this.quoteId,
contentType: newStatus.contentType,
language: newStatus.language,
poll: {},
preview: true
}).then((data) => {

View File

@ -194,6 +194,23 @@
: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
v-if="postFormats.length > 1"
class="text-format"

View File

@ -407,6 +407,15 @@
{{ $t('settings.preload_images') }}
</BooleanSetting>
</li>
<li>
<BooleanSetting
path="useBlurhash"
expert="1"
:disabled="!hideNsfw"
>
{{ $t('settings.use_blurhash') }}
</BooleanSetting>
</li>
<li>
<BooleanSetting
path="useOneClickNsfw"

View File

@ -30,7 +30,7 @@
.user-card {
position: relative;
z-index: 1;
z-index: 10;
&:hover {
--_still-image-img-visibility: visible;
@ -260,7 +260,7 @@
line-height: 22px;
flex-wrap: wrap;
.following, .requested_by {
.following, .requested_by, .blocking {
flex: 1 0 auto;
margin: 0;
margin-bottom: .25em;

View File

@ -127,6 +127,12 @@
</div>
</div>
<div class="user-meta">
<div
v-if="relationship.blocked_by && loggedIn && isOtherUser"
class="blocking"
>
{{ $t('user_card.blocks_you') }}
</div>
<div
v-if="relationship.followed_by && loggedIn && isOtherUser"
class="following"
@ -187,6 +193,7 @@
<FollowButton
:relationship="relationship"
:user="user"
:disabled="relationship.blocked_by"
/>
<template v-if="relationship.following">
<ProgressButton

View File

@ -10,11 +10,14 @@ import withLoadMore from '../../hocs/with_load_more/with_load_more'
import { debounce } from 'lodash'
import { library } from '@fortawesome/fontawesome-svg-core'
import {
faCircleNotch
faCircleNotch,
faCircleCheck
} from '@fortawesome/free-solid-svg-icons'
import FollowedTagCard from '../followed_tag_card/FollowedTagCard.vue'
library.add(
faCircleNotch
faCircleNotch,
faCircleCheck
)
const FollowerList = withLoadMore({
@ -33,6 +36,14 @@ const FriendList = withLoadMore({
additionalPropNames: ['userId']
})(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 UserProfile = {
@ -41,6 +52,7 @@ const UserProfile = {
error: false,
userId: null,
tab: 'statuses',
followsTab: 'users',
footerRef: null,
note: null,
noteLoading: false
@ -165,6 +177,9 @@ const UserProfile = {
this.tab = tab
this.$router.replace({ hash: `#${tab}` })
},
onFollowsTabSwitch (tab) {
this.followsTab = tab
},
linkClicked ({ target }) {
if (target.tagName === 'SPAN') {
target = target.parentNode
@ -200,6 +215,7 @@ const UserProfile = {
}
},
components: {
FollowedTagCard,
UserCard,
Timeline,
FollowerList,
@ -207,7 +223,8 @@ const UserProfile = {
FollowCard,
TabSwitcher,
Conversation,
RichContent
RichContent,
FollowedTagList
}
}

View File

@ -37,6 +37,15 @@
:html="field.value"
: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>
</dl>
</div>
@ -95,22 +104,48 @@
v-if="followsTabVisible"
key="followees"
:label="$t('user_card.followees')"
:disabled="!user.friends_count"
>
<FriendList :user-id="userId">
<template v-slot:item="{item}">
<FollowCard :user="item" />
</template>
</FriendList>
<tab-switcher
:active-tab="followsTab"
:render-only-focused="true"
:on-switch="onFollowsTabSwitch"
>
<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
v-if="followersTabVisible"
key="followers"
:label="$t('user_card.followers')"
:disabled="!user.followers_count"
>
<FollowerList :user-id="userId">
<template v-slot:item="{item}">
<template #item="{item}">
<FollowCard
:user="item"
:no-follows-you="isUs"
@ -225,6 +260,11 @@
padding: 0.5em 1.5em;
box-sizing: border-box;
}
.user-profile-field-validated {
margin-left: 1rem;
color: green;
}
}
}

View File

@ -59,7 +59,8 @@ const withLoadMore = ({
this.loading = false
this.bottomedOut = isEmpty(newEntries)
})
.catch(() => {
.catch((e) => {
console.error(e)
this.loading = false
this.error = true
})
@ -88,7 +89,7 @@ const withLoadMore = ({
const children = this.$slots
return (
<div class="with-load-more">
<WrappedComponent {...props}>
<WrappedComponent {...props} >
{children}
</WrappedComponent>
<div class="with-load-more-footer">

View File

@ -939,6 +939,7 @@
"title": "Version"
},
"virtual_scrolling": "Optimize timeline rendering",
"use_blurhash": "Use blurhashes for NSFW thumbnails",
"word_filter": "Word filter",
"wordfilter": "Wordfilter"
},
@ -1056,6 +1057,7 @@
"show_new": "Show new",
"socket_broke": "Realtime connection lost: CloseEvent code {0}",
"socket_reconnected": "Realtime connection established",
"follow_tag": "Follow hashtag",
"unfollow_tag": "Unfollow hashtag",
"up_to_date": "Up-to-date"
},
@ -1121,6 +1123,7 @@
"block_confirm_title": "Block user",
"block_progress": "Blocking…",
"blocked": "Blocked!",
"blocks_you": "Blocks you!",
"bot": "Bot",
"deactivated": "Deactivated",
"deny": "Deny",
@ -1138,6 +1141,8 @@
"follow_unfollow": "Unfollow",
"followees": "Following",
"followers": "Followers",
"followed_tags": "Followed hashtags",
"followed_users": "Followed users",
"following": "Following!",
"follows_you": "Follows you!",
"hidden": "Hidden",
@ -1176,6 +1181,9 @@
"unfollow_confirm_accept_button": "Yes, unfollow",
"unfollow_confirm_cancel_button": "No, don't unfollow",
"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_progress": "Unmuting…",
"unsubscribe": "Unsubscribe"
@ -1183,7 +1191,8 @@
"user_profile": {
"profile_does_not_exist": "Sorry, this profile does not exist.",
"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": {
"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:",

View File

@ -1,5 +1,6 @@
import backendInteractorService from '../services/backend_interactor_service/backend_interactor_service.js'
import { WSConnectionStatus } from '../services/api/api.service.js'
import { map } from 'lodash'
const retryTimeout = (multiplier) => 1000 * multiplier
@ -40,9 +41,6 @@ const api = {
setSocket (state, socket) {
state.socket = socket
},
setFollowRequests (state, value) {
state.followRequests = value
},
setMastoUserSocketStatus (state, value) {
state.mastoUserSocketStatus = value
},
@ -51,6 +49,15 @@ const api = {
},
resetRetryMultiplier (state) {
state.retryMultiplier = 1
},
setFollowRequests (state, value) {
state.followRequests = [...value]
},
saveFollowRequests (state, requests) {
state.followRequests = [...state.followRequests, ...requests]
},
saveFollowRequestPagination (state, pagination) {
state.followRequestsPagination = pagination
}
},
actions: {
@ -240,24 +247,22 @@ const api = {
...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) {
let requests = store.state.followRequests.filter((it) => it !== request)
let requests = [...store.state.followRequests].filter((it) => it.id !== request.id)
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
startFetchingLists (store) {
if (store.state.fetchers['lists']) return

View File

@ -117,7 +117,8 @@ export const defaultState = {
maxDepthInThread: undefined, // instance default
translationLanguage: undefined, // instance default,
supportedTranslationLanguages: {}, // instance default
userProfileDefaultTab: 'statuses'
userProfileDefaultTab: 'statuses',
useBlurhash: true,
}
// caching the instance default properties

View File

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

View File

@ -186,7 +186,7 @@ const interfaceMod = {
if (thirdColumnMode === 'none' || !rootState.users.currentUser) {
commit('setLayoutType', normalOrMobile)
} else {
const wideLayout = width >= 1300
const wideLayout = width >= 1280
commit('setLayoutType', wideLayout ? 'wide' : normalOrMobile)
}
},

View File

@ -2,9 +2,15 @@ import { merge } from 'lodash'
const tags = {
state: {
// Contains key = id, value = number of trackers for this poll
// Contains key = name, value = tag json
tags: {}
},
getters: {
findTag: state => query => {
const result = state.tags[query]
return result
},
},
mutations: {
setTag (state, { name, data }) {
state.tags[name] = data
@ -17,17 +23,17 @@ const tags = {
return tag
})
},
followTag (store, tagName) {
return store.rootState.api.backendInteractor.followHashtag({ tag: tagName })
followTag ({ rootState, commit }, tagName) {
return rootState.api.backendInteractor.followHashtag({ tag: tagName })
.then((resp) => {
store.commit('setTag', { name: tagName, data: resp })
commit('setTag', { name: tagName, data: resp })
return resp
})
},
unfollowTag ({ rootState, commit }, tag) {
return rootState.api.backendInteractor.unfollowHashtag({ tag })
unfollowTag ({ rootState, commit }, tagName) {
return rootState.api.backendInteractor.unfollowHashtag({ tag: tagName })
.then((resp) => {
commit('setTag', { name: tag, data: resp })
commit('setTag', { name: tagName, data: resp })
return resp
})
}

View File

@ -5,9 +5,9 @@ import { compact, map, each, mergeWith, last, concat, uniq, isArray } from 'loda
import { registerPushNotifications, unregisterPushNotifications } from '../services/push/push.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 }
const oldItem = obj[item.id]
const oldItem = obj[item[key]]
if (oldItem) {
// We already have this, so only merge the new info.
mergeWith(oldItem, item, mergeArrayLength)
@ -15,7 +15,7 @@ export const mergeOrAdd = (arr, obj, item) => {
} else {
// This is a new item, prepare it
arr.push(item)
obj[item.id] = item
obj[item[key]] = item
if (item.screen_name && !item.screen_name.includes('@')) {
obj[item.screen_name.toLowerCase()] = item
}
@ -157,6 +157,14 @@ export const mutations = {
const user = state.usersObject[id]
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
// outside of viewing someones user profile.
clearFriends (state, userId) {
@ -171,6 +179,12 @@ export const mutations = {
user['followerIds'] = []
}
},
clearFollowedTags (state, userId) {
const user = state.usersObject[userId]
if (user) {
user['followedTagIds'] = []
}
},
addNewUsers (state, users) {
each(users, (user) => {
if (user.relationship) {
@ -251,6 +265,12 @@ export const mutations = {
signUpFailure (state, errors) {
state.signUpPending = false
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 => {
const rel = id && state.relationships[id]
return rel || { id, loading: true }
}
},
}
export const defaultState = {
@ -282,7 +302,9 @@ export const defaultState = {
usersObject: {},
signUpPending: false,
signUpErrors: [],
relationships: {}
relationships: {},
tags: [],
tagsObject: {}
}
const users = {
@ -402,12 +424,27 @@ const users = {
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) {
commit('clearFriends', userId)
},
clearFollowers ({ commit }, userId) {
commit('clearFollowers', userId)
},
clearFollowedTags ({ commit }, userId) {
commit('clearFollowedTags', userId)
},
subscribeUser ({ rootState, commit }, id) {
return rootState.api.backendInteractor.subscribeUser({ id })
.then((relationship) => commit('updateUserRelationship', [relationship]))
@ -473,6 +510,12 @@ const users = {
store.commit('setUserForNotification', notification)
})
},
decrementFollowRequestsCount (store) {
store.commit('decrementFollowRequestsCount')
},
incrementFollowRequestsCount (store) {
store.commit('incrementFollowRequestsCount')
},
searchUsers ({ rootState, commit }, { query }) {
return rootState.api.backendInteractor.searchUsers({ query })
.then((users) => {
@ -536,7 +579,6 @@ const users = {
store.dispatch('stopFetchingTimeline', 'friends')
store.commit('setBackendInteractor', backendInteractorService(store.getters.getToken()))
store.dispatch('stopFetchingNotifications')
store.dispatch('stopFetchingFollowRequests')
store.dispatch('stopFetchingConfig')
store.commit('clearNotifications')
store.commit('resetStatuses')

View File

@ -1,6 +1,7 @@
import { each, map, concat, last, get } from 'lodash'
import { parseStatus, parseSource, parseUser, parseNotification, parseReport, parseAttachment, parseLinkHeaderPagination } from '../entity_normalizer/entity_normalizer.service.js'
import { RegistrationError, StatusCodeError } from '../errors/errors'
import { Url } from 'url'
/* eslint-env browser */
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 MOVE_ACCOUNT_URL = '/api/pleroma/move_account'
const ALIASES_URL = '/api/pleroma/aliases'
const TAG_USER_URL = '/api/pleroma/admin/users/tag'
const PERMISSION_GROUP_URL = (screenName, right) => `/api/pleroma/admin/users/${screenName}/permission_group/${right}`
const ACTIVATE_USER_URL = '/api/pleroma/admin/users/activate'
const DEACTIVATE_USER_URL = '/api/pleroma/admin/users/deactivate'
const ADMIN_USERS_URL = '/api/pleroma/admin/users'
const TAG_USER_URL = '/api/v1/pleroma/admin/users/tag'
const PERMISSION_GROUP_URL = (screenName, right) => `/api/v1/pleroma/admin/users/${screenName}/permission_group/${right}`
const ACTIVATE_USER_URL = '/api/v1/pleroma/admin/users/activate'
const DEACTIVATE_USER_URL = '/api/v1/pleroma/admin/users/deactivate'
const ADMIN_USERS_URL = '/api/v1/pleroma/admin/users'
const SUGGESTIONS_URL = '/api/v1/suggestions'
const NOTIFICATION_SETTINGS_URL = '/api/pleroma/notification_settings'
const NOTIFICATION_READ_URL = '/api/v1/pleroma/notifications/read'
@ -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_FOLLOW_TAG_URL = (name) => `/api/v1/tags/${name}/follow`
const MASTODON_UNFOLLOW_TAG_URL = (name) => `/api/v1/tags/${name}/unfollow`
const MASTODON_FOLLOWED_TAGS_URL = '/api/v1/followed_tags'
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) => {
if (accessToken) {
@ -404,14 +406,6 @@ const fetchFollowers = ({ id, maxId, sinceId, limit = 20, credentials }) => {
.then((data) => data.json())
.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 url = MASTODON_LISTS_URL
return fetch(url, { headers: authHeaders(credentials) })
@ -878,7 +872,8 @@ const postStatus = ({
quoteId,
contentType,
preview,
idempotencyKey
idempotencyKey,
language
}) => {
const form = new FormData()
const pollOptions = poll.options || []
@ -889,6 +884,7 @@ const postStatus = ({
if (visibility) form.append('visibility', visibility)
if (sensitive) form.append('sensitive', sensitive)
if (contentType) form.append('content_type', contentType)
if (language) form.append('language', language)
mediaIds.forEach(val => {
form.append('media_ids[]', val)
})
@ -1335,7 +1331,7 @@ const fetchEmojiReactions = ({ id, credentials }) => {
const reactWithEmoji = ({ id, emoji, credentials }) => {
return promisedRequest({
url: PLEROMA_EMOJI_REACT_URL(id, emoji),
url: PLEROMA_EMOJI_REACT_URL(id, encodeURIComponent(emoji)),
method: 'PUT',
credentials
}).then(parseStatus)
@ -1343,7 +1339,7 @@ const reactWithEmoji = ({ id, emoji, credentials }) => {
const unreactWithEmoji = ({ id, emoji, credentials }) => {
return promisedRequest({
url: PLEROMA_EMOJI_UNREACT_URL(id, emoji),
url: PLEROMA_EMOJI_UNREACT_URL(id, encodeURIComponent(emoji)),
method: 'DELETE',
credentials
}).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 = {} }) => {
return Object.entries({
...(credentials
@ -1764,7 +1802,6 @@ const apiService = {
mfaConfirmOTP,
addBackup,
listBackups,
fetchFollowRequests,
fetchLists,
createList,
getList,
@ -1813,7 +1850,9 @@ const apiService = {
deleteNoteFromReport,
getHashtag,
followHashtag,
unfollowHashtag
unfollowHashtag,
getFollowedHashtags,
getFollowRequests
}
export default apiService

View File

@ -1,7 +1,6 @@
import apiService, { getMastodonSocketURI, ProcessedWS } from '../api/api.service.js'
import timelineFetcher from '../timeline_fetcher/timeline_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 announcementsFetcher from '../../services/announcements_fetcher/announcements_fetcher.service.js'
import configFetcher from '../config_fetcher/config_fetcher.service.js'
@ -28,10 +27,6 @@ const backendInteractorService = credentials => ({
return notificationsFetcher.fetchAndUpdate({ ...args, credentials })
},
startFetchingFollowRequests ({ store }) {
return followRequestFetcher.startFetching({ store, credentials })
},
startFetchingLists ({ store }) {
return listsFetcher.startFetching({ store, credentials })
},

View File

@ -68,13 +68,15 @@ export const parseUser = (data) => {
output.fields_html = data.fields.map(field => {
return {
name: escape(field.name),
value: field.value
value: field.value,
verified_at: field.verified_at
}
})
output.fields_text = data.fields.map(field => {
return {
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.bot = data.bot
output.follow_requests_count = data.follow_requests_count
if (data.akkoma) {
output.instance = data.akkoma.instance
output.status_ttl_days = data.akkoma.status_ttl_days
@ -232,13 +235,14 @@ export const parseAttachment = (data) => {
if (masto) {
// Not exactly same...
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
} else {
output.mimetype = data.mimetype
// output.meta = ??? missing
}
output.blurhash = data.blurhash
output.url = data.url
output.large_thumb_url = data.preview_url
output.description = data.description
@ -408,8 +412,10 @@ export const parseNotification = (data) => {
if (masto) {
output.type = mastoDict[data.type] || data.type
output.seen = data.pleroma.is_seen
output.status = isStatusNotification(output.type) ? parseStatus(data.status) : null
output.action = output.status // TODO: Refactor, this is unneeded
if (data.status) {
output.status = isStatusNotification(output.type) ? parseStatus(data.status) : null
output.action = output.status // TODO: Refactor, this is unneeded
}
output.target = output.type !== 'move'
? null
: parseUser(data.target)

View File

@ -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

View File

@ -13,7 +13,8 @@ const postStatus = ({
quoteId = undefined,
contentType = 'text/plain',
preview = false,
idempotencyKey = ''
idempotencyKey = '',
language
}) => {
const mediaIds = map(media, 'id')
@ -29,7 +30,8 @@ const postStatus = ({
contentType,
poll,
preview,
idempotencyKey
idempotencyKey,
language
})
.then((data) => {
if (!data.error && !preview) {

View File

@ -4,12 +4,10 @@ import { getColors, computeDynamicColor, getOpacitySlot } from '../theme_data/th
export const applyTheme = (input) => {
const { rules } = generatePreset(input)
const head = document.head
const body = document.body
body.classList.add('hidden')
const styleEl = document.createElement('style')
head.appendChild(styleEl)
const styleEl = document.getElementById('theme-holder')
const styleSheet = styleEl.sheet
styleSheet.toString()

View File

@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.8 KiB

1
static/theme-holder.css Normal file
View File

@ -0,0 +1 @@
// This file intentionally left blank

View File

@ -1350,6 +1350,13 @@
minimatch "^3.0.4"
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":
version "6.2.0"
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/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":
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"
@ -2494,6 +2494,11 @@ binary-extensions@^2.0.0:
resolved "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz"
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:
version "1.19.2"
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"
integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==
iso-639-1@2.1.15:
iso-639-1@^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==
isobject@^3.0.1: