Compare commits

...

51 Commits

Author SHA1 Message Date
FloatingGhost 3c36845f2a Add DM settings 2023-07-09 05:27:08 +00:00
Denys Nykula 9519cfc298 fix unfinished post being sent when scrolling 2023-07-09 05:26:50 +00:00
Denys Nykula 02a53d5662 fix apply theme button without page refresh 2023-07-09 05:26:35 +00:00
Denys Nykula ceb8fc7f16 fix dropdown-item-icon and form controls using missing variables 2023-07-09 05:26:22 +00:00
FloatingGhost e154aca3fa ensure we only fetch reports when we're an admin
Ref #288
2023-07-09 05:23:46 +00:00
FloatingGhost 3bfc10f43a Remove unused bits and bobs 2023-07-09 05:19:46 +00:00
flisk ac6459aca9 add recently used emojis panel to emoji picker (#283)
~~(not intended for merging yet, just submitting this for preliminary review and discussion)~~

this patch adds a tab with recently used emojis to the emoji picker: https://akko.lain.gay/notice/ASoGCtyoiXbYPJjqpk

there's a couple of things i'm ~~still trying to work out~~ not totally happy with and i'd appreciate any feedback on them:

* the recentEmojis getter is called very frequently and has to do a possibly somewhat expensive lookup of emoji objects by their `displayName` each time, which i'm not sure is ideal
* ~~emoji reactions on posts added through the picker are picked up by the recentEmojis module, but clicks on existing emoji reactions are not, because `addReaction` in `react_button.js` only currently receives the replacement and not the full emoji object (if there even is one wherever that method is called from)~~ this works now and does the same stupid full search of all emojis by their name which i guess is less bad because this only happens when you hit a reaction emoji button that already existed

Reviewed-on: #283
Co-authored-by: flisk <akkomadev.mvch71fq@flisk.xyz>
Co-committed-by: flisk <akkomadev.mvch71fq@flisk.xyz>
2023-04-30 07:06:49 +00:00
solidsanek 9a8136e31c Drafts: Fix drafts erasing edits and redrafts 2023-04-30 07:06:24 +00:00
solidsanek 282a37ad6a Post: remove debug logs 2023-04-30 07:06:07 +00:00
solidsanek 055b04ee8f Post: Add drafting feature 2023-04-30 07:05:55 +00:00
eris b60bcbd06d Revert "add language input"
This reverts commit 68f2b0cf8e.
2023-02-28 07:38:25 +00:00
eris 75ace34da1 Revert "Add blurhash support"
This reverts commit 1edc1a2ec7.
2023-02-28 07:38:13 +00:00
eris 8b91ccc830 Revert "Fall back to nsfw image if no blurhash"
This reverts commit 3e19a6091f.
2023-02-28 07:38:01 +00:00
yanchan09 1619c11812 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: #275
Co-authored-by: yanchan09 <yan@omg.lol>
Co-committed-by: yanchan09 <yan@omg.lol>
2023-02-28 07:04:00 +00:00
floatingghost ed94118499 paginate-follow-requests (#277)
Co-authored-by: FloatingGhost <hannah@coffee-and-dreams.uk>
Reviewed-on: #277
2023-02-28 07:03:49 +00:00
FloatingGhost 68f2b0cf8e add language input 2023-02-28 07:03:28 +00:00
Sol Fisher Romanoff e2c0fd26d6 Only show "keep open" emoji checkbox on post form 2023-02-28 07:03:15 +00:00
FloatingGhost 544c536d82 Remove console.log 2023-02-28 07:03:01 +00:00
FloatingGhost 5df921a537 add follow/unfollow to followed tags list 2023-02-28 07:02:52 +00:00
FloatingGhost 4ae1297319 Add list of followed hashtags to profile 2023-02-28 07:02:40 +00:00
floatingghost 3e19a6091f Fall back to nsfw image if no blurhash 2023-02-28 06:52:42 +00:00
FloatingGhost 1edc1a2ec7 Add blurhash support 2023-02-28 06:52:26 +00:00
FloatingGhost 2ea987b766 remove IHBA assets 2023-02-28 06:51:54 +00:00
Beefox ad018f4b72 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: #246
Co-authored-by: Beefox <bee@beefox.xyz>
Co-committed-by: Beefox <bee@beefox.xyz>
2023-02-28 06:51:40 +00:00
Sol Fisher Romanoff 5838112c40 Remove stray debug log 2023-02-28 06:50:54 +00:00
FloatingGhost 0b6ba5ea6b force CI build 2023-02-28 06:50:32 +00:00
FloatingGhost e0b180f34e update readme 2023-02-28 06:50:19 +00:00
FloatingGhost cfebd058b3 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.
2023-02-28 06:49:56 +00:00
FloatingGhost 643c6943cb add verification of links 2023-02-28 06:46:18 +00:00
FloatingGhost 13a792cb5a fix emoji picker in replies in notifications 2023-02-28 06:46:03 +00:00
FloatingGhost 131a7967f1 don't crash out if notification status is null 2023-02-28 06:45:40 +00:00
FloatingGhost 638ce80113 Make everything work with a strict CSP 2023-02-28 06:36:51 +00:00
eris 7ffa39784b Disable follow button if blocked by user 2023-01-27 00:26:50 +00:00
eris 092c94301f Add indicator if user blocks you 2023-01-26 20:50:52 +00:00
eris 9dc054343a Temporary fix for moderation tools 2022-12-23 19:25:30 +00:00
eris a1d56dfe50 Better Edited at styling 2022-12-23 19:04:52 +00:00
eris 27a91e4f7c Icon changes 2022-12-17 05:53:37 +00:00
eris 61d82ec7a7 Change default for site name 2022-12-16 20:34:28 +00:00
Norm 170ea7771b adjust max-width for emoji picker to new 1280px 3-col width 2022-12-16 20:31:16 +00:00
Norm 04081a09c2 Make minimum width for 3-column layout 1280px
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
2022-12-16 20:30:49 +00:00
eris bb685ff61c Locale changes and default settings update 2022-12-16 07:21:33 +00:00
eris a0062cd4be Move bubble timeline and change icon 2022-12-16 06:53:35 +00:00
eris 3c478bd8d6 Redo about page and staff panel 2022-12-16 06:12:28 +00:00
eris e1437255c2 Adjust user mention avatars and enable by default 2022-12-16 06:11:18 +00:00
eris 311860cde5 Strip displaying MRF features that dont respect MRF transparency settings 2022-12-16 06:09:51 +00:00
eris ee633216fd Fix emoji rendering 2022-12-16 06:09:29 +00:00
eris 69b1f25497 Fix emoji picker issues, add header 2022-12-16 06:08:08 +00:00
eris 5d26f2a7bf Fix image description weirdness, render alt text properly 2022-12-16 06:06:22 +00:00
eris eb9e3278f3 Misskey-like quote styling 2022-12-16 06:05:30 +00:00
eris 40ffad5bc7 Update pleromafe version link 2022-12-16 06:04:13 +00:00
eris c5e3677072 Disable replies and media tabs if not logged in 2022-12-16 06:03:32 +00:00
71 changed files with 996 additions and 494 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

@ -4,12 +4,11 @@
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1,user-scalable=no">
<title>Akkoma</title>
<link rel="stylesheet" href="/static/font/css/fontello.css">
<link rel="stylesheet" href="/static/font/css/animation.css">
<link rel="stylesheet" href="/static/font/tiresias.css">
<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

@ -22,7 +22,7 @@
"@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",
"@floatingghost/pinch-zoom-element": "^1.3.1",
"@vuelidate/core": "^2.0.0",
"@vuelidate/validators": "^2.0.0",
"body-scroll-lock": "2.7.1",

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;
@ -468,7 +469,7 @@ textarea,
color: $fallback--lightText;
color: var(--inputText, $fallback--lightText);
font-family: sans-serif;
font-family: var(--inputFont, sans-serif);
font-family: var(--interfaceFont, sans-serif);
font-size: 1em;
margin: 0;
box-sizing: border-box;

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'
@ -394,9 +396,6 @@ const afterStoreSetup = async ({ store, i18n }) => {
])
// Start fetching things that don't need to block the UI
store.dispatch('fetchMutes')
store.dispatch('startFetchingAnnouncements')
store.dispatch('startFetchingReports')
getTOS({ store })
getStickers({ store })

View File

@ -1,6 +1,6 @@
<template>
<div class="column-inner">
<instance-specific-panel v-if="showInstanceSpecificPanel" />
<!--<instance-specific-panel v-if="showInstanceSpecificPanel" />-->
<staff-panel />
<terms-of-service-panel />
<LocalBubblePanel v-if="showLocalBubblePanel" />

View File

@ -4,7 +4,7 @@ import { library } from '@fortawesome/fontawesome-svg-core'
import {
faSignInAlt,
faSignOutAlt,
faHome,
faHouseChimney,
faComments,
faUserPlus,
faBullhorn,
@ -23,7 +23,7 @@ import {
library.add(
faSignInAlt,
faSignOutAlt,
faHome,
faHouseChimney,
faComments,
faUserPlus,
faBullhorn,
@ -98,15 +98,11 @@ export default {
logoLeft () { return this.$store.state.instance.logoLeft },
currentUser () { return this.$store.state.users.currentUser },
privateMode () { return this.$store.state.instance.private },
federating () { return this.$store.state.instance.federating },
shouldConfirmLogout () {
return this.$store.getters.mergedConfig.modalOnLogout
},
showBubbleTimeline () {
return this.$store.state.instance.localBubbleInstances.length > 0
},
restrictedTimelines () {
return this.$store.state.instance.restrict_unauthenticated.timelines
}
},
methods: {

View File

@ -39,12 +39,11 @@
<FAIcon
fixed-width
class="fa-scale-110 fa-old-padding"
icon="home"
icon="house-chimney"
:title="$t('nav.home_timeline')"
/>
</router-link>
<router-link
v-if="currentUser || !(privateMode || restrictedTimelines.public)"
:to="{ name: 'public-timeline' }"
class="nav-icon"
>
@ -56,19 +55,6 @@
/>
</router-link>
<router-link
v-if="currentUser && showBubbleTimeline"
:to="{ name: 'bubble-timeline' }"
class="nav-icon"
>
<FAIcon
fixed-width
class="fa-scale-110 fa-old-padding"
icon="circle"
:title="$t('nav.bubble_timeline')"
/>
</router-link>
<router-link
v-if="federating && (currentUser || !(privateMode || restrictedTimelines.federated))"
:to="{ name: 'public-external-timeline' }"
class="nav-icon"
>
@ -79,6 +65,18 @@
:title="$t('nav.twkn')"
/>
</router-link>
<router-link
v-if="currentUser && showBubbleTimeline"
:to="{ name: 'bubble-timeline' }"
class="nav-icon"
>
<FAIcon
fixed-width
class="fa-scale-110 fa-old-padding"
icon="comment-medical"
:title="$t('nav.bubble_timeline')"
/>
</router-link>
</div>
</div>
<router-link

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"
@ -88,9 +89,10 @@
}
}
.emoji-picker-panel {
position: absolute;
position: relative;
z-index: 20;
margin-top: 2px;
top: 0px !important;
&.hide {
display: none

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) {
@ -55,12 +51,7 @@ const EmojiPicker = {
onEmoji (emoji) {
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)
this.$store.commit('emojiUsed', emoji)
},
onWheel (e) {
e.preventDefault()
@ -69,68 +60,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 +81,6 @@ const EmojiPicker = {
})
}
},
watch: {
keyword () {
this.customEmojiLoadAllConfirmed = false
this.onScroll()
this.startEmojiLoad(true)
}
},
computed: {
activeGroupView () {
return this.showingStickers ? '' : this.activeGroup
@ -168,10 +96,8 @@ const EmojiPicker = {
this.$store.state.instance.customEmoji || []
)
},
customEmojiBuffer () {
return this.filteredEmoji.slice(0, this.customEmojiBufferSlice)
},
emojis () {
const recentEmojis = this.$store.getters.recentEmojis
const standardEmojis = this.$store.state.instance.emoji || []
const customEmojis = this.sortedEmoji
const emojiPacks = []
@ -184,6 +110,15 @@ const EmojiPicker = {
})
})
return [
{
id: 'recent',
text: this.$t('emoji.recent'),
first: {
imageUrl: '',
replacement: '🕒',
},
emojis: this.filterByKeyword(recentEmojis)
},
{
id: 'standard',
text: this.$t('emoji.unicode'),

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 {
@ -56,7 +71,11 @@
.heading {
margin-top: 10px;
height: 4.8em;
height: 5.8em;
}
.emoji-header {
margin-left: 5px;
}
.content {
@ -70,10 +89,6 @@
flex-grow: 1;
}
.emoji-groups {
min-height: 200px;
}
.additional-tabs {
border-left: 1px solid;
border-left-color: $fallback--icon;
@ -152,76 +167,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

@ -1,10 +1,11 @@
<template>
<div class="emoji-picker panel panel-default panel-body">
<div class="heading">
<span class="emoji-header">Emoji Packs</span>
<span
ref="emoji-tabs"
class="emoji-tabs"
@wheel="onWheel"
ref="emoji-tabs"
>
<span
v-for="group in emojis"
@ -51,40 +52,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

@ -3,6 +3,11 @@ import UserListPopover from '../user_list_popover/user_list_popover.vue'
const EMOJI_REACTION_COUNT_CUTOFF = 12
const findEmojiByReplacement = (state, replacement) => {
const allEmojis = state.instance.emoji.concat(state.instance.customEmoji)
return allEmojis.find(emoji => emoji.replacement === replacement)
}
const EmojiReactions = {
name: 'EmojiReactions',
components: {
@ -54,6 +59,8 @@ const EmojiReactions = {
},
reactWith (emoji) {
this.$store.dispatch('reactWithEmoji', { id: this.status.id, emoji })
const emojiObject = findEmojiByReplacement(this.$store.state, emoji)
this.$store.commit('emojiUsed', emojiObject)
},
unreact (emoji) {
this.$store.dispatch('unreactWithEmoji', { id: this.status.id, emoji })

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

@ -188,9 +188,21 @@ $modal-view-button-icon-margin: 0.5em;
overflow-y: auto;
min-height: 1em;
max-width: 500px;
max-height: 9.5em;
word-break: break-word;
white-space: pre-line;
background: rgba(0,0,0,0.6);
padding: 10px;
border-radius: 10px;
transition: 0.5s;
&:not(:hover) {
max-height: 1em;
}
&:hover {
max-height: 9.5em;
transition: 1s;
}
}
.modal-image {

View File

@ -15,11 +15,15 @@
.mention-avatar {
border-radius: var(--avatarAltRadius, $fallback--avatarAltRadius);
width: 1.5em;
height: 1.5em;
width: 2em;
height: 2em;
vertical-align: middle;
user-select: none;
margin-right: 0.2em;
.still-image.avatar {
border-radius: 14px;
}
}
.full {
@ -113,3 +117,11 @@
color: var(--faint, $fallback--faint);
}
}
.reply-glued-label {
.mention-avatar {
width: 1.4em;
height: 1.4em;
}
}

View File

@ -41,11 +41,10 @@
{{ $t('user_card.admin_menu.delete_account') }}
</button>
<div
v-if="hasTagPolicy"
role="separator"
class="dropdown-divider"
/>
<span v-if="hasTagPolicy">
<span>
<button
class="button-default dropdown-item"
@click="toggleTag(tags.FORCE_NSFW)"

View File

@ -76,7 +76,7 @@
</table>
</div>
<div v-if="quarantineInstances.length">
<!--<div v-if="quarantineInstances.length">
<h4>{{ $t("about.mrf.simple.quarantine") }}</h4>
<p>{{ $t("about.mrf.simple.quarantine_desc") }}</p>
@ -99,7 +99,7 @@
</td>
</tr>
</table>
</div>
</div>-->
<div v-if="ftlRemovalInstances.length">
<h4>{{ $t("about.mrf.simple.ftl_removal") }}</h4>
@ -176,7 +176,7 @@
</table>
</div>
<h2 v-if="hasKeywordPolicies">
<!--<h2 v-if="hasKeywordPolicies">
{{ $t("about.mrf.keyword.keyword_policies") }}
</h2>
@ -217,7 +217,7 @@
{{ keyword.replacement }}
</li>
</ul>
</div>
</div>-->
</div>
</div>
</div>

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

@ -114,7 +114,7 @@
svg {
width: 22px;
margin-right: 0.75rem;
color: var(--menuPopoverIcon, $fallback--icon)
color: var(--popoverIcon, $fallback--icon)
}
}

View File

@ -53,6 +53,14 @@ const pxStringToNumber = (str) => {
return Number(str.substring(0, str.length - 2))
}
const deleteDraft = (draftKey) => {
const draftData = JSON.parse(localStorage.getItem('drafts') || '{}');
delete draftData[draftKey];
localStorage.setItem('drafts', JSON.stringify(draftData));
}
const PostStatusForm = {
props: [
'statusId',
@ -157,6 +165,36 @@ const PostStatusForm = {
}
}
if (!this.statusId) {
let draftKey = 'status';
if (this.replyTo) {
draftKey = 'reply:' + this.replyTo;
} else if (this.quoteId) {
draftKey = 'quote:' + this.quoteId;
}
const draft = JSON.parse(localStorage.getItem('drafts') || '{}')[draftKey];
if (draft) {
statusParams = {
spoilerText: draft.data.spoilerText,
status: draft.data.status,
sensitiveIfSubject,
nsfw: draft.data.nsfw,
files: draft.data.files,
poll: draft.data.poll,
mediaDescriptions: draft.data.mediaDescriptions,
visibility: draft.data.visibility,
language: draft.data.language,
contentType: draft.data.contentType
}
if (draft.data.poll) {
this.togglePollForm();
}
}
}
return {
dropFiles: [],
uploadingFiles: false,
@ -273,6 +311,7 @@ const PostStatusForm = {
statusChanged () {
this.autoPreview()
this.updateIdempotencyKey()
this.saveDraft()
},
clearStatus () {
const newStatus = this.newStatus
@ -391,8 +430,38 @@ const PostStatusForm = {
}).finally(() => {
this.previewLoading = false
})
let draftKey = 'status';
if (this.replyTo) {
draftKey = 'reply:' + this.replyTo;
} else if (this.quoteId) {
draftKey = 'quote:' + this.quoteId;
}
deleteDraft(draftKey)
},
debouncePreviewStatus: debounce(function () { this.previewStatus() }, 500),
saveDraft() {
const draftData = JSON.parse(localStorage.getItem('drafts') || '{}');
let draftKey = 'status';
if (this.replyTo) {
draftKey = 'reply:' + this.replyTo;
} else if (this.quoteId) {
draftKey = 'quote:' + this.quoteId;
}
if (this.newStatus.status || this.newStatus.spoilerText || this.newStatus.files.length > 0 || this.newStatus.poll.length > 0) {
draftData[draftKey] = {
updatedAt: new Date(),
data: this.newStatus,
};
localStorage.setItem('drafts', JSON.stringify(draftData));
} else {
deleteDraft(draftKey);
}
},
autoPreview () {
if (!this.preview) return
this.previewLoading = true

View File

@ -274,12 +274,14 @@
>
{{ $t('post_status.post') }}
</button>
<!-- touchstart is used to keep the OSK at the same position after a message send -->
<!-- To keep the OSK at the same position after a message send, -->
<!-- @touchstart.stop.prevent was used. But while OSK position is -->
<!-- quirky, accidental mobile posts caused by the workaround -->
<!-- when people tried to scroll were a more serious bug. -->
<button
v-else
:disabled="uploadingFiles || disableSubmit"
class="btn button-default"
@touchstart.stop.prevent="postStatus($event, newStatus)"
@click.stop.prevent="postStatus($event, newStatus)"
>
{{ $t('post_status.post') }}

View File

@ -64,11 +64,11 @@
color: $fallback--text;
color: var(--text, $fallback--text);
border-style: solid;
border-style: dashed;
border-width: 1px;
border-radius: $fallback--attachmentRadius;
border-radius: var(--attachmentRadius, $fallback--attachmentRadius);
border-color: $fallback--border;
border-color: var(--border, $fallback--border);
border-color: $fallback--cBlue;
border-color: var(--cBlue, $fallback--cBlue);
}
</style>

View File

@ -49,7 +49,7 @@
}
.emoji {
display: inline-block;
display: inline-flex;
width: var(--emoji-size, 32px);
height: var(--emoji-size, 32px);
}

View File

@ -2,6 +2,7 @@ import { library } from '@fortawesome/fontawesome-svg-core'
import {
faEnvelope,
faLock,
faBiohazard,
faLockOpen,
faGlobe
} from '@fortawesome/free-solid-svg-icons'
@ -9,6 +10,7 @@ import {
library.add(
faEnvelope,
faGlobe,
faBiohazard,
faLock,
faLockOpen
)

View File

@ -64,7 +64,7 @@
@click="changeVis('local')"
>
<FAIcon
icon="users"
icon="biohazard"
class="fa-scale-110 fa-old-padding"
/>
</button>

View File

@ -38,7 +38,7 @@ label.Select {
margin: 0;
padding: 0 2em 0 .2em;
font-family: sans-serif;
font-family: var(--inputFont, sans-serif);
font-family: var(--interfaceFont, sans-serif);
font-size: 1em;
width: 100%;
z-index: 1;

View File

@ -21,7 +21,6 @@
>
{{ $t('settings.settings_profile_force_sync') }}
</button>
</p>
<div
@click="toggleExpandedSettings"

View File

@ -12,6 +12,7 @@ import InterfaceLanguageSwitcher from 'src/components/interface_language_switche
import BooleanSetting from '../helpers/boolean_setting.vue'
import SharedComputedObject from '../helpers/shared_computed_object.js'
import localeService from 'src/services/locale/locale.service.js'
import ChoiceSetting from '../helpers/choice_setting.vue'
import { library } from '@fortawesome/fontawesome-svg-core'
import {
@ -46,9 +47,16 @@ const ProfileTab = {
emailLanguage: this.$store.state.users.currentUser.language || '',
newPostTTLDays: this.$store.state.users.currentUser.status_ttl_days,
expirePosts: this.$store.state.users.currentUser.status_ttl_days !== null,
userAcceptsDirectMessagesFrom: this.$store.state.users.currentUser.accepts_direct_messages_from,
userAcceptsDirectMessagesFromOptions: ["everybody", "nobody", "people_i_follow"].map(mode => ({
key: mode,
value: mode,
label: this.$t(`settings.user_accepts_direct_messages_from_${mode}`)
}))
}
},
components: {
ChoiceSetting,
ScopeSelector,
ImageCropper,
EmojiInput,
@ -126,7 +134,8 @@ const ProfileTab = {
fields_attributes: this.newFields.filter(el => el != null),
bot: this.bot,
show_role: this.showRole,
status_ttl_days: this.expirePosts ? this.newPostTTLDays : -1
status_ttl_days: this.expirePosts ? this.newPostTTLDays : -1,
accepts_direct_messages_from: this.userAcceptsDirectMessagesFrom
/* eslint-enable camelcase */
}

View File

@ -89,6 +89,15 @@
{{ $t('settings.bot') }}
</Checkbox>
</p>
<p>
<ChoiceSetting
id="userAcceptsDirectMessagesFrom"
path="userAcceptsDirectMessagesFrom"
:options="userAcceptsDirectMessagesFromOptions"
>
{{ $t('settings.user_accepts_direct_messages_from') }}
</ChoiceSetting>
</p>
<p>
<Checkbox v-model="expirePosts">
{{ $t('settings.expire_posts_enabled') }}
@ -102,6 +111,9 @@
class="expire-posts-days"
:placeholder="$t('settings.expire_posts_input_placeholder')"
/>
</p>
<p>
</p>
<p>
<interface-language-switcher

View File

@ -89,6 +89,10 @@
margin: 1em 1em 0;
}
.presets {
text-align: center;
}
.tab-header {
display: flex;
justify-content: space-between;

View File

@ -1,6 +1,6 @@
import { extractCommit } from 'src/services/version/version.service'
const pleromaFeCommitUrl = 'https://akkoma.dev/AkkomaGang/pleroma-fe/commit/'
const pleromaFeCommitUrl = 'https://akkoma.dev/eris/pleroma-fe/commit/'
const pleromaBeCommitUrl = 'https://akkoma.dev/AkkomaGang/akkoma/commit/'
const VersionTab = {

View File

@ -12,7 +12,7 @@
:key="group.role"
class="staff-group"
>
<h4>{{ $t('general.role.' + group.role) }}</h4>
<h4>The Admin Team</h4>
<basic-user-card
v-for="user in group.users"
:key="user.screen_name"
@ -30,10 +30,21 @@
.staff-group {
padding-left: 1em;
padding-top: 1em;
padding-top: 0.2em;
padding-bottom: 2px;
.basic-user-card {
padding: 0.6em 0.4em;
padding-left: 0;
display: inline-block;
}
.basic-user-card-collapsed-content {
display: none;
}
.still-image {
border-radius: 15px;
}
}

View File

@ -27,6 +27,7 @@ import {
faLock,
faLockOpen,
faGlobe,
faBiohazard,
faTimes,
faRetweet,
faReply,
@ -46,6 +47,7 @@ library.add(
faEnvelope,
faGlobe,
faLock,
faBiohazard,
faLockOpen,
faTimes,
faRetweet,
@ -455,7 +457,7 @@ const Status = {
case 'direct':
return 'envelope'
case 'local':
return 'users'
return 'biohazard'
default:
return 'globe'
}

View File

@ -193,6 +193,26 @@
:with-direction="true"
:auto-update="60"
/>
<i18n-t
v-if="isEdited && editingAvailable && !isPreview"
keypath="status.edited_at"
tag="span"
class="edited-row"
>
<template #time>
<i18n-t
keypath="time.in_past"
tag="span"
>
<template>
<Timeago
:time="status.edited_at"
:auto-update="60"
/>
</template>
</i18n-t>
</template>
</i18n-t>
</router-link>
<span
v-if="status.visibility"
@ -332,24 +352,6 @@
class="mentions-line"
/>
</div>
<div
v-if="isEdited && editingAvailable && !isPreview"
class="heading-edited-row"
>
<i18n-t
keypath="status.edited_at"
tag="span"
>
<template #time>
<Timeago
:time="status.edited_at"
:auto-update="60"
:long-format="true"
:with-direction="true"
/>
</template>
</i18n-t>
</div>
</div>
<div class="content">

View File

@ -93,4 +93,9 @@
}
}
}
.still-image.emoji {
img {
height: unset;
}
}
</style>

View File

@ -5,8 +5,8 @@ import {
faGlobe,
faBookmark,
faEnvelope,
faHome,
faCircle
faHouseChimney,
faCommentMedical
} from '@fortawesome/free-solid-svg-icons'
library.add(
@ -14,8 +14,8 @@ library.add(
faGlobe,
faBookmark,
faEnvelope,
faHome,
faCircle
faHouseChimney,
faCommentMedical
)
const TimelineMenuContent = {
@ -24,8 +24,7 @@ const TimelineMenuContent = {
currentUser: state => state.users.currentUser,
privateMode: state => state.instance.private,
federating: state => state.instance.federating,
showBubbleTimeline: state => (state.instance.localBubbleInstances.length > 0),
restrictedTimelines: state => state.instance.restrict_unauthenticated.timelines
showBubbleTimeline: state => (state.instance.localBubbleInstances.length > 0)
})
}
}

View File

@ -8,7 +8,7 @@
<FAIcon
fixed-width
class="fa-scale-110 fa-old-padding "
icon="home"
icon="house-chimney"
/>
<span
:title="$t('nav.home_timeline_description')"
@ -16,23 +16,7 @@
>{{ $t("nav.home_timeline") }}</span>
</router-link>
</li>
<li v-if="currentUser && showBubbleTimeline">
<router-link
class="menu-item"
:to="{ name: 'bubble-timeline' }"
>
<FAIcon
fixed-width
class="fa-scale-110 fa-old-padding "
icon="circle"
/>
<span
:title="$t('nav.bubble_timeline_description')"
:aria-label="$t('nav.bubble_timeline_description')"
>{{ $t("nav.bubble_timeline") }}</span>
</router-link>
</li>
<li v-if="currentUser || !(privateMode || restrictedTimelines.public)">
<li v-if="currentUser || !privateMode">
<router-link
class="menu-item"
:to="{ name: 'public-timeline' }"
@ -48,7 +32,7 @@
>{{ $t("nav.public_tl") }}</span>
</router-link>
</li>
<li v-if="federating && (currentUser || !(privateMode || restrictedTimelines.federated))">
<li v-if="federating && (currentUser || !privateMode)">
<router-link
class="menu-item"
:to="{ name: 'public-external-timeline' }"
@ -64,6 +48,22 @@
>{{ $t("nav.twkn") }}</span>
</router-link>
</li>
<li v-if="currentUser && showBubbleTimeline">
<router-link
class="menu-item"
:to="{ name: 'bubble-timeline' }"
>
<FAIcon
fixed-width
class="fa-scale-110 fa-old-padding "
icon="comment-medical"
/>
<span
:title="$t('nav.bubble_timeline_description')"
:aria-label="$t('nav.bubble_timeline_description')"
>{{ $t("nav.bubble_timeline") }}</span>
</router-link>
</li>
<li v-if="currentUser">
<router-link
class="menu-item"

View File

@ -235,7 +235,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>
@ -90,27 +99,54 @@
:user-id="userId"
:in-profile="true"
:footer-slipgate="footerRef"
:disabled="!currentUser"
/>
<div
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"
@ -128,6 +164,7 @@
:user-id="userId"
:in-profile="true"
:footer-slipgate="footerRef"
:disabled="!currentUser"
/>
<Timeline
v-if="isUs"
@ -225,6 +262,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

@ -1,11 +1,11 @@
{
"about": {
"bubble_instances": "Local Bubble Instances",
"bubble_instances": "Recommended Instances",
"bubble_instances_description": "Instances chosen by the admins to represent the local area of this instance",
"mrf": {
"federation": "Federation",
"keyword": {
"ftl_removal": "Removal from \"The Whole Known Network\" Timeline",
"ftl_removal": "Removal from Federated Timeline",
"is_replaced_by": "→",
"keyword_policies": "Keyword policies",
"reject": "Reject",
@ -16,8 +16,8 @@
"simple": {
"accept": "Accept",
"accept_desc": "This instance only accepts messages from the following instances:",
"ftl_removal": "Removal from \"Known Network\" Timeline",
"ftl_removal_desc": "This instance removes these instances from \"Known Network\" timeline:",
"ftl_removal": "Removal from Federated Timeline",
"ftl_removal_desc": "This instance removes these instances from the federated timeline:",
"instance": "Instance",
"media_nsfw": "Media force-set as sensitive",
"media_nsfw_desc": "This instance forces media to be set sensitive in posts on the following instances:",
@ -27,8 +27,8 @@
"quarantine": "Quarantine",
"quarantine_desc": "This instance will not send posts to the following instances:",
"reason": "Reason",
"reject": "Reject",
"reject_desc": "This instance will not accept messages from the following instances:",
"reject": "Instance Blocks",
"reject_desc": "This instance blocks messages to and from the following instances:",
"simple_policies": "Instance-specific policies"
}
},
@ -86,7 +86,8 @@
"load_all_hint": "Loaded first {saneAmount} emoji, loading all emoji may cause performance issues.",
"search_emoji": "Search for an emoji",
"stickers": "Stickers",
"unicode": "Unicode emoji"
"unicode": "Unicode emoji",
"recent": "Recently used"
},
"errors": {
"storage_unavailable": "Pleroma could not access browser storage. Your login or your local settings won't be saved and you might encounter unexpected issues. Try enabling cookies."
@ -302,12 +303,12 @@
"announcements": "Announcements",
"back": "Back",
"bookmarks": "Bookmarks",
"bubble_timeline": "Bubble timeline",
"bubble_timeline": "Recommended Instances",
"bubble_timeline_description": "Posts from instances close to yours, as recommended by the admins",
"chats": "Chats",
"dms": "Direct messages",
"friend_requests": "Follow requests",
"home_timeline": "Home timeline",
"dms": "Direct Messages",
"friend_requests": "Follow Requests",
"home_timeline": "Home Timeline",
"home_timeline_description": "Posts from people you follow",
"interactions": "Interactions",
"lists": "Lists",
@ -315,11 +316,11 @@
"moderation": "Moderation",
"preferences": "Preferences",
"public_timeline_description": "Public posts from this instance",
"public_tl": "Public timeline",
"public_tl": "Local Timeline",
"search": "Search",
"timeline": "Timeline",
"timelines": "Timelines",
"twkn": "Known Network",
"twkn": "Federated Timeline",
"twkn_timeline_description": "Posts from the entire network",
"user_search": "User Search",
"who_to_follow": "Who to follow"
@ -379,7 +380,7 @@
"text/x.misskeymarkdown": "MFM"
},
"content_warning": "Content Warning (optional)",
"default": "Just arrived at Luna Nova Academy",
"default": "Just landed on Neptune",
"direct_warning_to_all": "This post will be visible to all the mentioned users.",
"direct_warning_to_first_only": "This post will only be visible to the mentioned users at the beginning of the message.",
"edit_remote_warning": "Changes made to the post may not be visible on some instances!",
@ -928,6 +929,10 @@
"user_profile_default_tab": "Default Tab on User Profile",
"user_profiles": "User Profiles",
"user_settings": "User Settings",
"user_accepts_direct_messages_from": "Accept DMs From",
"user_accepts_direct_messages_from_everybody": "Everybody",
"user_accepts_direct_messages_from_nobody": "Nobody",
"user_accepts_direct_messages_from_people_i_follow": "People I follow",
"valid_until": "Valid until",
"values": {
"false": "no",
@ -963,7 +968,7 @@
"edit": "Edit",
"edit_history": "Edit History",
"edit_history_modal_title": "Edited {historyCount} time | Edited {historyCount} times",
"edited_at": "Edited {time}",
"edited_at": " (Edited {time})",
"expand": "Expand",
"external_source": "External source",
"favorites": "Favorites",
@ -1056,6 +1061,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 +1127,7 @@
"block_confirm_title": "Block user",
"block_progress": "Blocking…",
"blocked": "Blocked!",
"blocks_you": "Blocks you!",
"bot": "Bot",
"deactivated": "Deactivated",
"deny": "Deny",
@ -1138,6 +1145,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 +1185,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 +1195,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

@ -19,7 +19,8 @@ const saveImmedeatelyActions = [
'setOption',
'setClientData',
'setToken',
'clearToken'
'clearToken',
'emojiUsed',
]
const defaultStorage = (() => {

View File

@ -22,6 +22,7 @@ import announcementsModule from './modules/announcements.js'
import editStatusModule from './modules/editStatus.js'
import statusHistoryModule from './modules/statusHistory.js'
import tagModule from './modules/tags.js'
import recentEmojisModule from './modules/recentEmojis.js'
import { createI18n } from 'vue-i18n'
@ -47,7 +48,8 @@ const persistedStateOptions = {
paths: [
'config',
'users.lastLoginName',
'oauth'
'oauth',
'recentEmojis.emojis',
]
};
@ -98,7 +100,8 @@ const persistedStateOptions = {
announcements: announcementsModule,
editStatus: editStatusModule,
statusHistory: statusHistoryModule,
tags: tagModule
tags: tagModule,
recentEmojis: recentEmojisModule,
},
plugins,
strict: false // Socket modifies itself, let's ignore this for now.

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

@ -52,11 +52,11 @@ export const defaultState = {
loopVideoSilentOnly: true,
streaming: false,
emojiReactionsOnTimeline: true,
alwaysShowNewPostButton: false,
alwaysShowNewPostButton: true,
autohideFloatingPostButton: false,
pauseOnUnfocused: true,
stopGifs: undefined,
replyVisibility: 'all',
replyVisibility: 'following',
thirdColumnMode: 'notifications',
notificationVisibility: {
follows: true,
@ -98,7 +98,7 @@ export const defaultState = {
useAtIcon: undefined, // instance default
mentionLinkDisplay: undefined, // instance default
mentionLinkShowTooltip: undefined, // instance default
mentionLinkShowAvatar: undefined, // instance default
mentionLinkShowAvatar: true, // instance default
mentionLinkFadeDomain: undefined, // instance default
mentionLinkShowYous: undefined, // instance default
mentionLinkBoldenYou: undefined, // instance default

View File

@ -20,11 +20,11 @@ const defaultState = {
defaultBanner: '/images/banner.png',
background: '/static/aurora_borealis.jpg',
collapseMessageWithSubject: true,
greentext: false,
greentext: true,
useAtIcon: false,
mentionLinkDisplay: 'short',
mentionLinkShowTooltip: true,
mentionLinkShowAvatar: false,
mentionLinkShowAvatar: true,
mentionLinkFadeDomain: true,
mentionLinkShowYous: false,
mentionLinkBoldenYou: true,
@ -60,7 +60,7 @@ const defaultState = {
showFeaturesPanel: true,
showInstanceSpecificPanel: false,
showNavShortcuts: true,
showWiderShortcuts: true,
showWiderShortcuts: false,
sidebarRight: false,
subjectLineBehavior: 'email',
theme: 'pleroma-dark',

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

@ -0,0 +1,50 @@
// each row is 7 emojis, 6 rows chosen arbitrarily. i don't think more than
// that are going to be useful.
const RECENT_MAX = 7 * 6
const defaultState = {
emojis: [],
}
const recentEmojis = {
state: defaultState,
mutations: {
emojiUsed ({ emojis }, emoji) {
if (emoji.displayText === undefined || emoji.displayText === null) {
console.error('emojiUsed was called with a bad emoji object: ', emoji)
return
} else if (emoji.displayText.includes('@')) {
console.error('emojiUsed was called with a remote emoji: ', emoji)
return
}
const i = emojis.indexOf(emoji.displayText)
if (i === -1) {
// not in `emojis` yet, insert and truncate if necessary
const newLength = emojis.unshift(emoji.displayText)
if (newLength > RECENT_MAX) {
emojis.pop()
}
} else if (i !== 0) {
// emoji is already in `emojis` but needs to be bumped to the top
emojis.splice(i, 1)
emojis.unshift(emoji.displayText)
}
},
},
getters: {
recentEmojis: (state, getters, rootState) => state.emojis.reduce((objects, displayText) => {
const allEmojis = rootState.instance.emoji.concat(rootState.instance.customEmoji)
let emojiObject = allEmojis.find(emoji => emoji.displayText === displayText)
if (emojiObject !== undefined) {
objects.push(emojiObject)
}
return objects
}, []),
},
}
export default recentEmojis

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')
@ -595,13 +637,16 @@ const users = {
// Get user mutes
store.dispatch('fetchMutes')
store.dispatch('setLayoutWidth', windowWidth())
store.dispatch('setLayoutHeight', windowHeight())
store.dispatch('getSupportedTranslationlanguages')
store.dispatch('getSettingsProfile')
store.dispatch('listSettingsProfiles')
store.dispatch('startFetchingConfig')
store.dispatch('startFetchingAnnouncements')
if (user.role === 'admin' || user.role === 'moderator') {
store.dispatch('startFetchingReports')
}
// Fetch our friends
store.rootState.api.backendInteractor.fetchFriends({ id: user.id })

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'
@ -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
@ -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) })
@ -1575,6 +1569,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 +1800,6 @@ const apiService = {
mfaConfirmOTP,
addBackup,
listBackups,
fetchFollowRequests,
fetchLists,
createList,
getList,
@ -1813,7 +1848,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,8 @@ export const parseUser = (data) => {
output.friends_count = data.following_count
output.bot = data.bot
output.accepts_direct_messages_from = data.accepts_direct_messages_from
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
@ -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

@ -4,19 +4,21 @@ 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 styleSheet = styleEl.sheet
/** @type {CSSStyleSheet} */
const styleSheet = document.getElementById('theme-holder').sheet
for (let i = styleSheet.cssRules.length; i--; ) {
styleSheet.deleteRule(0)
}
styleSheet.insertRule(
`:root { ${rules.radii}; ${rules.colors}; ${rules.shadows}; ${rules.fonts}; }`,
0
)
styleSheet.toString()
styleSheet.insertRule(`:root { ${rules.radii} }`, 'index-max')
styleSheet.insertRule(`:root { ${rules.colors} }`, 'index-max')
styleSheet.insertRule(`:root { ${rules.shadows} }`, 'index-max')
styleSheet.insertRule(`:root { ${rules.fonts} }`, 'index-max')
body.classList.remove('hidden')
}

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;" />

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"