Compare commits

...

51 commits

Author SHA1 Message Date
3c36845f2a Add DM settings 2023-07-09 05:27:08 +00:00
9519cfc298 fix unfinished post being sent when scrolling 2023-07-09 05:26:50 +00:00
02a53d5662 fix apply theme button without page refresh 2023-07-09 05:26:35 +00:00
ceb8fc7f16 fix dropdown-item-icon and form controls using missing variables 2023-07-09 05:26:22 +00:00
e154aca3fa ensure we only fetch reports when we're an admin
Ref #288
2023-07-09 05:23:46 +00:00
3bfc10f43a Remove unused bits and bobs 2023-07-09 05:19:46 +00:00
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
b60bcbd06d Revert "add language input"
This reverts commit 68f2b0cf8e.
2023-02-28 07:38:25 +00:00
75ace34da1 Revert "Add blurhash support"
This reverts commit 1edc1a2ec7.
2023-02-28 07:38:13 +00:00
8b91ccc830 Revert "Fall back to nsfw image if no blurhash"
This reverts commit 3e19a6091f.
2023-02-28 07:38:01 +00:00
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
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
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
544c536d82 Remove console.log 2023-02-28 07:03:01 +00:00
5df921a537 add follow/unfollow to followed tags list 2023-02-28 07:02:52 +00:00
4ae1297319 Add list of followed hashtags to profile 2023-02-28 07:02:40 +00:00
3e19a6091f Fall back to nsfw image if no blurhash 2023-02-28 06:52:42 +00:00
1edc1a2ec7 Add blurhash support 2023-02-28 06:52:26 +00:00
2ea987b766 remove IHBA assets 2023-02-28 06:51:54 +00:00
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
0b6ba5ea6b force CI build 2023-02-28 06:50:32 +00:00
e0b180f34e update readme 2023-02-28 06:50:19 +00:00
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
643c6943cb add verification of links 2023-02-28 06:46:18 +00:00
13a792cb5a fix emoji picker in replies in notifications 2023-02-28 06:46:03 +00:00
131a7967f1 don't crash out if notification status is null 2023-02-28 06:45:40 +00:00
638ce80113 Make everything work with a strict CSP 2023-02-28 06:36:51 +00:00
7ffa39784b Disable follow button if blocked by user 2023-01-27 00:26:50 +00:00
092c94301f Add indicator if user blocks you 2023-01-26 20:50:52 +00:00
9dc054343a Temporary fix for moderation tools 2022-12-23 19:25:30 +00:00
a1d56dfe50 Better Edited at styling 2022-12-23 19:04:52 +00:00
27a91e4f7c Icon changes 2022-12-17 05:53:37 +00:00
61d82ec7a7 Change default for site name 2022-12-16 20:34:28 +00:00
170ea7771b adjust max-width for emoji picker to new 1280px 3-col width 2022-12-16 20:31:16 +00:00
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
bb685ff61c Locale changes and default settings update 2022-12-16 07:21:33 +00:00
a0062cd4be Move bubble timeline and change icon 2022-12-16 06:53:35 +00:00
3c478bd8d6 Redo about page and staff panel 2022-12-16 06:12:28 +00:00
e1437255c2 Adjust user mention avatars and enable by default 2022-12-16 06:11:18 +00:00
311860cde5 Strip displaying MRF features that dont respect MRF transparency settings 2022-12-16 06:09:51 +00:00
ee633216fd Fix emoji rendering 2022-12-16 06:09:29 +00:00
69b1f25497 Fix emoji picker issues, add header 2022-12-16 06:08:08 +00:00
5d26f2a7bf Fix image description weirdness, render alt text properly 2022-12-16 06:06:22 +00:00
eb9e3278f3 Misskey-like quote styling 2022-12-16 06:05:30 +00:00
40ffad5bc7 Update pleromafe version link 2022-12-16 06:04:13 +00:00
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) ![English OK](https://img.shields.io/badge/English-OK-blueviolet) ![日本語OK](https://img.shields.io/badge/%E6%97%A5%E6%9C%AC%E8%AA%9E-OK-blueviolet)
This is a fork of Pleroma-FE from the Pleroma project, with support for new Akkoma features such as: This is a fork of Akkoma-FE from the Pleroma project, with support for new Akkoma features such as:
- MFM support via [marked-mfm](https://akkoma.dev/sfr/marked-mfm) - MFM support via [marked-mfm](https://akkoma.dev/sfr/marked-mfm)
- Custom emoji reactions - Custom emoji reactions
# For Translators # For Translators
The [Weblate UI](https://translate.akkoma.dev/projects/akkoma/pleroma-fe/) is recommended for adding or modifying translations for Pleroma-FE. The [Weblate UI](https://translate.akkoma.dev/projects/akkoma/pleroma-fe/) is recommended for adding or modifying translations for Akkoma-FE.
Alternatively, edit/create `src/i18n/$LANGUAGE_CODE.json` (where `$LANGUAGE_CODE` is the [ISO 639-1 code](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes) for your language), then add your language to [src/i18n/messages.js](https://akkoma.dev/AkkomaGang/pleroma-fe/src/branch/develop/src/i18n/messages.js) if it doesn't already exist there. Alternatively, edit/create `src/i18n/$LANGUAGE_CODE.json` (where `$LANGUAGE_CODE` is the [ISO 639-1 code](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes) for your language), then add your language to [src/i18n/messages.js](https://akkoma.dev/AkkomaGang/pleroma-fe/src/branch/develop/src/i18n/messages.js) if it doesn't already exist there.
Pleroma-FE will set your language by your browser locale, but you can temporarily force it in the code by changing the locale in main.js. Akkoma-FE will set your language by your browser locale, but you can temporarily force it in the code by changing the locale in main.js.
# FOR ADMINS # FOR ADMINS
To use Pleroma-FE in Akkoma, use the [frontend](https://docs.akkoma.dev/stable/administration/CLI_tasks/frontend/) CLI task to install Pleroma-FE, then modify your configuration as described in the [Frontend Management](https://docs.akkoma.dev/stable/configuration/frontend_management/) doc. To use Akkoma-FE in Akkoma, use the [frontend](https://docs.akkoma.dev/stable/administration/CLI_tasks/frontend/) CLI task to install Akkoma-FE, then modify your configuration as described in the [Frontend Management](https://docs.akkoma.dev/stable/configuration/frontend_management/) doc.
## Build Setup ## Build Setup
@ -52,4 +52,4 @@ Edit config.json for configuration.
### Login methods ### Login methods
```loginMethod``` can be set to either ```password``` (the default) or ```token```, which will use the full oauth redirection flow, which is useful for SSO situations. ```loginMethod``` can be set to either ```password``` (the default) or ```token```, which will use the full oauth redirection flow, which is useful for SSO situations.

View file

@ -4,12 +4,11 @@
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1,user-scalable=no"> <meta name="viewport" content="width=device-width, initial-scale=1,user-scalable=no">
<title>Akkoma</title> <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/tiresias.css">
<link rel="stylesheet" href="/static/font/css/lato.css"> <link rel="stylesheet" href="/static/font/css/lato.css">
<link rel="stylesheet" href="/static/mfm.css"> <link rel="stylesheet" href="/static/mfm.css">
<link rel="stylesheet" href="/static/custom.css"> <link rel="stylesheet" href="/static/custom.css">
<link rel="stylesheet" href="/static/theme-holder.css" id="theme-holder">
<!--server-generated-meta--> <!--server-generated-meta-->
<link rel="icon" type="image/png" href="/favicon.png"> <link rel="icon" type="image/png" href="/favicon.png">
<link rel="manifest" href="/manifest.json"> <link rel="manifest" href="/manifest.json">

View file

@ -22,7 +22,7 @@
"@fortawesome/free-regular-svg-icons": "^6.1.2", "@fortawesome/free-regular-svg-icons": "^6.1.2",
"@fortawesome/free-solid-svg-icons": "^6.2.0", "@fortawesome/free-solid-svg-icons": "^6.2.0",
"@fortawesome/vue-fontawesome": "3.0.1", "@fortawesome/vue-fontawesome": "3.0.1",
"@kazvmoe-infra/pinch-zoom-element": "1.2.0", "@floatingghost/pinch-zoom-element": "^1.3.1",
"@vuelidate/core": "^2.0.0", "@vuelidate/core": "^2.0.0",
"@vuelidate/validators": "^2.0.0", "@vuelidate/validators": "^2.0.0",
"body-scroll-lock": "2.7.1", "body-scroll-lock": "2.7.1",

View file

@ -1,6 +1,7 @@
// stylelint-disable rscss/class-format // stylelint-disable rscss/class-format
@import './_variables.scss'; @import './_variables.scss';
@import '@fortawesome/fontawesome-svg-core/styles.css';
@import '@floatingghost/pinch-zoom-element/dist/pinch-zoom.css';
:root { :root {
--navbar-height: 3.5rem; --navbar-height: 3.5rem;
--post-line-height: 1.4; --post-line-height: 1.4;
@ -468,7 +469,7 @@ textarea,
color: $fallback--lightText; color: $fallback--lightText;
color: var(--inputText, $fallback--lightText); color: var(--inputText, $fallback--lightText);
font-family: sans-serif; font-family: sans-serif;
font-family: var(--inputFont, sans-serif); font-family: var(--interfaceFont, sans-serif);
font-size: 1em; font-size: 1em;
margin: 0; margin: 0;
box-sizing: border-box; box-sizing: border-box;

View file

@ -4,6 +4,8 @@ import { createRouter, createWebHistory } from 'vue-router'
import vClickOutside from 'click-outside-vue3' import vClickOutside from 'click-outside-vue3'
import { FontAwesomeIcon, FontAwesomeLayers } from '@fortawesome/vue-fontawesome' import { FontAwesomeIcon, FontAwesomeLayers } from '@fortawesome/vue-fontawesome'
import { config } from '@fortawesome/fontawesome-svg-core';
config.autoAddCss = false
import App from '../App.vue' import App from '../App.vue'
import routes from './routes' import routes from './routes'
@ -394,9 +396,6 @@ const afterStoreSetup = async ({ store, i18n }) => {
]) ])
// Start fetching things that don't need to block the UI // Start fetching things that don't need to block the UI
store.dispatch('fetchMutes')
store.dispatch('startFetchingAnnouncements')
store.dispatch('startFetchingReports')
getTOS({ store }) getTOS({ store })
getStickers({ store }) getStickers({ store })

View file

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

View file

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

View file

@ -39,12 +39,11 @@
<FAIcon <FAIcon
fixed-width fixed-width
class="fa-scale-110 fa-old-padding" class="fa-scale-110 fa-old-padding"
icon="home" icon="house-chimney"
:title="$t('nav.home_timeline')" :title="$t('nav.home_timeline')"
/> />
</router-link> </router-link>
<router-link <router-link
v-if="currentUser || !(privateMode || restrictedTimelines.public)"
:to="{ name: 'public-timeline' }" :to="{ name: 'public-timeline' }"
class="nav-icon" class="nav-icon"
> >
@ -56,19 +55,6 @@
/> />
</router-link> </router-link>
<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' }" :to="{ name: 'public-external-timeline' }"
class="nav-icon" class="nav-icon"
> >
@ -79,6 +65,18 @@
:title="$t('nav.twkn')" :title="$t('nav.twkn')"
/> />
</router-link> </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>
</div> </div>
<router-link <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 () { triggerShowPicker () {
this.showPicker = true this.showPicker = true
this.$refs.picker.startEmojiLoad()
this.$nextTick(() => { this.$nextTick(() => {
this.scrollIntoView() this.scrollIntoView()
this.focusPickerInput() this.focusPickerInput()
@ -223,7 +222,6 @@ const EmojiInput = {
this.showPicker = !this.showPicker this.showPicker = !this.showPicker
if (this.showPicker) { if (this.showPicker) {
this.scrollIntoView() this.scrollIntoView()
this.$refs.picker.startEmojiLoad()
this.$nextTick(this.focusPickerInput) this.$nextTick(this.focusPickerInput)
} }
}, },

View file

@ -18,6 +18,7 @@
<EmojiPicker <EmojiPicker
v-if="enableEmojiPicker" v-if="enableEmojiPicker"
ref="picker" ref="picker"
show-keep-open
:class="{ hide: !showPicker }" :class="{ hide: !showPicker }"
:enable-sticker-picker="enableStickerPicker" :enable-sticker-picker="enableStickerPicker"
class="emoji-picker-panel" class="emoji-picker-panel"
@ -88,9 +89,10 @@
} }
} }
.emoji-picker-panel { .emoji-picker-panel {
position: absolute; position: relative;
z-index: 20; z-index: 20;
margin-top: 2px; margin-top: 2px;
top: 0px !important;
&.hide { &.hide {
display: none display: none

View file

@ -1,5 +1,6 @@
import { defineAsyncComponent } from 'vue' import { defineAsyncComponent } from 'vue'
import Checkbox from '../checkbox/checkbox.vue' import Checkbox from '../checkbox/checkbox.vue'
import EmojiGrid from '../emoji_grid/emoji_grid.vue'
import { library } from '@fortawesome/fontawesome-svg-core' import { library } from '@fortawesome/fontawesome-svg-core'
import { import {
faBoxOpen, faBoxOpen,
@ -14,19 +15,17 @@ library.add(
faSmileBeam faSmileBeam
) )
// At widest, approximately 20 emoji are visible in a row,
// loading 3 rows, could be overkill for narrow picker
const LOAD_EMOJI_BY = 60
// When to start loading new batch emoji, in pixels
const LOAD_EMOJI_MARGIN = 64
const EmojiPicker = { const EmojiPicker = {
props: { props: {
enableStickerPicker: { enableStickerPicker: {
required: false, required: false,
type: Boolean, type: Boolean,
default: false default: false
},
showKeepOpen: {
required: false,
type: Boolean,
default: false
} }
}, },
data () { data () {
@ -34,16 +33,13 @@ const EmojiPicker = {
keyword: '', keyword: '',
activeGroup: 'standard', activeGroup: 'standard',
showingStickers: false, showingStickers: false,
groupsScrolledClass: 'scrolled-top', keepOpen: false
keepOpen: false,
customEmojiBufferSlice: LOAD_EMOJI_BY,
customEmojiTimeout: null,
customEmojiLoadAllConfirmed: false
} }
}, },
components: { components: {
StickerPicker: defineAsyncComponent(() => import('../sticker_picker/sticker_picker.vue')), StickerPicker: defineAsyncComponent(() => import('../sticker_picker/sticker_picker.vue')),
Checkbox Checkbox,
EmojiGrid
}, },
methods: { methods: {
onStickerUploaded (e) { onStickerUploaded (e) {
@ -55,12 +51,7 @@ const EmojiPicker = {
onEmoji (emoji) { onEmoji (emoji) {
const value = emoji.imageUrl ? `:${emoji.displayText}:` : emoji.replacement const value = emoji.imageUrl ? `:${emoji.displayText}:` : emoji.replacement
this.$emit('emoji', { insertion: value, keepOpen: this.keepOpen }) this.$emit('emoji', { insertion: value, keepOpen: this.keepOpen })
}, this.$store.commit('emojiUsed', emoji)
onScroll (e) {
const target = (e && e.target) || this.$refs['emoji-groups']
this.updateScrolledClass(target)
this.scrolledGroup(target)
this.triggerLoadMore(target)
}, },
onWheel (e) { onWheel (e) {
e.preventDefault() e.preventDefault()
@ -69,68 +60,12 @@ const EmojiPicker = {
highlight (key) { highlight (key) {
this.setShowStickers(false) this.setShowStickers(false)
this.activeGroup = key this.activeGroup = key
}, if (this.keyword.length) {
updateScrolledClass (target) { this.$refs.emojiGrid.scrollToItem(key)
if (target.scrollTop <= 5) {
this.groupsScrolledClass = 'scrolled-top'
} else if (target.scrollTop >= target.scrollTopMax - 5) {
this.groupsScrolledClass = 'scrolled-bottom'
} else {
this.groupsScrolledClass = 'scrolled-middle'
} }
}, },
triggerLoadMore (target) { onActiveGroup (group) {
const ref = this.$refs['group-end-custom'] this.activeGroup = group
if (!ref) return
const bottom = ref.offsetTop + ref.offsetHeight
const scrollerBottom = target.scrollTop + target.clientHeight
const scrollerTop = target.scrollTop
const scrollerMax = target.scrollHeight
// Loads more emoji when they come into view
const approachingBottom = bottom - scrollerBottom < LOAD_EMOJI_MARGIN
// Always load when at the very top in case there's no scroll space yet
const atTop = scrollerTop < 5
// Don't load when looking at unicode category or at the very bottom
const bottomAboveViewport = bottom < scrollerTop || scrollerBottom === scrollerMax
if (!bottomAboveViewport && (approachingBottom || atTop)) {
this.loadEmoji()
}
},
scrolledGroup (target) {
const top = target.scrollTop + 5
this.$nextTick(() => {
this.emojisView.forEach(group => {
const ref = this.$refs['group-' + group.id]
if (ref.offsetTop <= top) {
this.activeGroup = group.id
}
})
})
},
loadEmoji () {
const allLoaded = this.customEmojiBuffer.length === this.filteredEmoji.length
if (allLoaded) {
return
}
this.customEmojiBufferSlice += LOAD_EMOJI_BY
},
startEmojiLoad (forceUpdate = false) {
if (!forceUpdate) {
this.keyword = ''
}
this.$nextTick(() => {
this.$refs['emoji-groups'].scrollTop = 0
})
const bufferSize = this.customEmojiBuffer.length
const bufferPrefilledAll = bufferSize === this.filteredEmoji.length
if (bufferPrefilledAll && !forceUpdate) {
return
}
this.customEmojiBufferSlice = LOAD_EMOJI_BY
}, },
toggleStickers () { toggleStickers () {
this.showingStickers = !this.showingStickers this.showingStickers = !this.showingStickers
@ -146,13 +81,6 @@ const EmojiPicker = {
}) })
} }
}, },
watch: {
keyword () {
this.customEmojiLoadAllConfirmed = false
this.onScroll()
this.startEmojiLoad(true)
}
},
computed: { computed: {
activeGroupView () { activeGroupView () {
return this.showingStickers ? '' : this.activeGroup return this.showingStickers ? '' : this.activeGroup
@ -168,10 +96,8 @@ const EmojiPicker = {
this.$store.state.instance.customEmoji || [] this.$store.state.instance.customEmoji || []
) )
}, },
customEmojiBuffer () {
return this.filteredEmoji.slice(0, this.customEmojiBufferSlice)
},
emojis () { emojis () {
const recentEmojis = this.$store.getters.recentEmojis
const standardEmojis = this.$store.state.instance.emoji || [] const standardEmojis = this.$store.state.instance.emoji || []
const customEmojis = this.sortedEmoji const customEmojis = this.sortedEmoji
const emojiPacks = [] const emojiPacks = []
@ -184,6 +110,15 @@ const EmojiPicker = {
}) })
}) })
return [ return [
{
id: 'recent',
text: this.$t('emoji.recent'),
first: {
imageUrl: '',
replacement: '🕒',
},
emojis: this.filterByKeyword(recentEmojis)
},
{ {
id: 'standard', id: 'standard',
text: this.$t('emoji.unicode'), text: this.$t('emoji.unicode'),

View file

@ -1,5 +1,16 @@
@import '../../_variables.scss'; @import '../../_variables.scss';
// The worst query selector ever
// selects ONLY emojis pickers in replies in notifications
// who thought this was a good idea?
.notification > .Status > .status-container > .post-status-form > form > .form-group > .emoji-input > .emoji-picker {
max-width: 100%;
left: 0;
@media (min-width: 1300px) {
left: -30px;
}
}
.Notification { .Notification {
.emoji-picker { .emoji-picker {
min-width: 160%; min-width: 160%;
@ -7,7 +18,7 @@
overflow: hidden; overflow: hidden;
left: -70%; left: -70%;
max-width: 100%; max-width: 100%;
@media (min-width: 800px) and (max-width: 1300px) { @media (min-width: 800px) and (max-width: 1280px) {
left: -50%; left: -50%;
min-width: 50%; min-width: 50%;
max-width: 130%; max-width: 130%;
@ -18,6 +29,10 @@
min-width: 50%; min-width: 50%;
max-width: 130%; max-width: 130%;
} }
.Status > .emoji-picker {
z-index: 1000;
}
} }
} }
.emoji-picker { .emoji-picker {
@ -56,7 +71,11 @@
.heading { .heading {
margin-top: 10px; margin-top: 10px;
height: 4.8em; height: 5.8em;
}
.emoji-header {
margin-left: 5px;
} }
.content { .content {
@ -70,10 +89,6 @@
flex-grow: 1; flex-grow: 1;
} }
.emoji-groups {
min-height: 200px;
}
.additional-tabs { .additional-tabs {
border-left: 1px solid; border-left: 1px solid;
border-left-color: $fallback--icon; border-left-color: $fallback--icon;
@ -152,76 +167,12 @@
} }
} }
.emoji { .emoji-search {
&-search { padding: 5px;
padding: 5px; flex: 0 0 auto;
flex: 0 0 auto;
input { input {
width: 100%; width: 100%;
}
} }
&-groups {
flex: 1 1 1px;
position: relative;
overflow: auto;
user-select: none;
mask: linear-gradient(to top, white 0, transparent 100%) bottom no-repeat,
linear-gradient(to bottom, white 0, transparent 100%) top no-repeat,
linear-gradient(to top, white, white);
transition: mask-size 150ms;
mask-size: 100% 20px, 100% 20px, auto;
// Autoprefixed seem to ignore this one, and also syntax is different
-webkit-mask-composite: xor;
mask-composite: exclude;
&.scrolled {
&-top {
mask-size: 100% 20px, 100% 0, auto;
}
&-bottom {
mask-size: 100% 0, 100% 20px, auto;
}
}
}
&-group {
display: flex;
align-items: center;
flex-wrap: wrap;
padding-left: 5px;
justify-content: left;
&-title {
font-size: 0.85em;
width: 100%;
margin: 0;
&.disabled {
display: none;
}
}
}
&-item {
width: 32px;
height: 32px;
box-sizing: border-box;
display: flex;
font-size: 32px;
align-items: center;
justify-content: center;
margin: 4px;
cursor: pointer;
img {
object-fit: contain;
max-width: 100%;
max-height: 100%;
}
}
} }
} }

View file

@ -1,10 +1,11 @@
<template> <template>
<div class="emoji-picker panel panel-default panel-body"> <div class="emoji-picker panel panel-default panel-body">
<div class="heading"> <div class="heading">
<span class="emoji-header">Emoji Packs</span>
<span <span
ref="emoji-tabs"
class="emoji-tabs" class="emoji-tabs"
@wheel="onWheel" @wheel="onWheel"
ref="emoji-tabs"
> >
<span <span
v-for="group in emojis" v-for="group in emojis"
@ -51,40 +52,16 @@
@input="$event.target.composing = false" @input="$event.target.composing = false"
> >
</div> </div>
<EmojiGrid
ref="emojiGrid"
:groups="emojisView"
@emoji="onEmoji"
@active-group="onActiveGroup"
/>
<div <div
ref="emoji-groups" v-if="showKeepOpen"
class="emoji-groups" class="keep-open"
:class="groupsScrolledClass"
@scroll="onScroll"
> >
<div
v-for="group in emojisView"
:key="group.id"
class="emoji-group"
>
<h6
:ref="'group-' + group.id"
class="emoji-group-title"
>
{{ group.text }}
</h6>
<span
v-for="emoji in group.emojis"
:key="group.id + emoji.displayText"
:title="emoji.displayText"
class="emoji-item"
@click.stop.prevent="onEmoji(emoji)"
>
<span v-if="!emoji.imageUrl">{{ emoji.replacement }}</span>
<img
v-else
:src="emoji.imageUrl"
>
</span>
<span :ref="'group-end-' + group.id" />
</div>
</div>
<div class="keep-open">
<Checkbox v-model="keepOpen"> <Checkbox v-model="keepOpen">
{{ $t('emoji.keep_open') }} {{ $t('emoji.keep_open') }}
</Checkbox> </Checkbox>

View file

@ -3,6 +3,11 @@ import UserListPopover from '../user_list_popover/user_list_popover.vue'
const EMOJI_REACTION_COUNT_CUTOFF = 12 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 = { const EmojiReactions = {
name: 'EmojiReactions', name: 'EmojiReactions',
components: { components: {
@ -54,6 +59,8 @@ const EmojiReactions = {
}, },
reactWith (emoji) { reactWith (emoji) {
this.$store.dispatch('reactWithEmoji', { id: this.status.id, emoji }) this.$store.dispatch('reactWithEmoji', { id: this.status.id, emoji })
const emojiObject = findEmojiByReplacement(this.$store.state, emoji)
this.$store.commit('emojiUsed', emojiObject)
}, },
unreact (emoji) { unreact (emoji) {
this.$store.dispatch('unreactWithEmoji', { id: this.status.id, emoji }) this.$store.dispatch('unreactWithEmoji', { id: this.status.id, emoji })

View file

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

View file

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

View file

@ -1,10 +1,26 @@
import FollowRequestCard from '../follow_request_card/follow_request_card.vue' import FollowRequestCard from '../follow_request_card/follow_request_card.vue'
import withLoadMore from '../../hocs/with_load_more/with_load_more'
import List from '../list/list.vue'
import get from 'lodash/get'
const FollowRequestList = withLoadMore({
fetch: (props, $store) => $store.dispatch('fetchFollowRequests'),
select: (props, $store) => get($store.state.api, 'followRequests', []).map(req => $store.getters.findUser(req.id)),
destroy: (props, $store) => $store.dispatch('clearFollowRequests'),
childPropName: 'items',
additionalPropNames: ['userId']
})(List);
const FollowRequests = { const FollowRequests = {
components: { components: {
FollowRequestCard FollowRequestCard,
FollowRequestList
}, },
computed: { computed: {
userId () {
return this.$store.state.users.currentUser.id
},
requests () { requests () {
return this.$store.state.api.followRequests return this.$store.state.api.followRequests
} }

View file

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

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; overflow-y: auto;
min-height: 1em; min-height: 1em;
max-width: 500px; max-width: 500px;
max-height: 9.5em;
word-break: break-word; word-break: break-word;
white-space: pre-line; 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 { .modal-image {

View file

@ -15,11 +15,15 @@
.mention-avatar { .mention-avatar {
border-radius: var(--avatarAltRadius, $fallback--avatarAltRadius); border-radius: var(--avatarAltRadius, $fallback--avatarAltRadius);
width: 1.5em; width: 2em;
height: 1.5em; height: 2em;
vertical-align: middle; vertical-align: middle;
user-select: none; user-select: none;
margin-right: 0.2em; margin-right: 0.2em;
.still-image.avatar {
border-radius: 14px;
}
} }
.full { .full {
@ -113,3 +117,11 @@
color: var(--faint, $fallback--faint); 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') }} {{ $t('user_card.admin_menu.delete_account') }}
</button> </button>
<div <div
v-if="hasTagPolicy"
role="separator" role="separator"
class="dropdown-divider" class="dropdown-divider"
/> />
<span v-if="hasTagPolicy"> <span>
<button <button
class="button-default dropdown-item" class="button-default dropdown-item"
@click="toggleTag(tags.FORCE_NSFW)" @click="toggleTag(tags.FORCE_NSFW)"

View file

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

View file

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

View file

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

View file

@ -114,7 +114,7 @@
svg { svg {
width: 22px; width: 22px;
margin-right: 0.75rem; 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)) 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 = { const PostStatusForm = {
props: [ props: [
'statusId', '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 { return {
dropFiles: [], dropFiles: [],
uploadingFiles: false, uploadingFiles: false,
@ -273,6 +311,7 @@ const PostStatusForm = {
statusChanged () { statusChanged () {
this.autoPreview() this.autoPreview()
this.updateIdempotencyKey() this.updateIdempotencyKey()
this.saveDraft()
}, },
clearStatus () { clearStatus () {
const newStatus = this.newStatus const newStatus = this.newStatus
@ -391,8 +430,38 @@ const PostStatusForm = {
}).finally(() => { }).finally(() => {
this.previewLoading = false 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), 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 () { autoPreview () {
if (!this.preview) return if (!this.preview) return
this.previewLoading = true this.previewLoading = true

View file

@ -274,12 +274,14 @@
> >
{{ $t('post_status.post') }} {{ $t('post_status.post') }}
</button> </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 <button
v-else v-else
:disabled="uploadingFiles || disableSubmit" :disabled="uploadingFiles || disableSubmit"
class="btn button-default" class="btn button-default"
@touchstart.stop.prevent="postStatus($event, newStatus)"
@click.stop.prevent="postStatus($event, newStatus)" @click.stop.prevent="postStatus($event, newStatus)"
> >
{{ $t('post_status.post') }} {{ $t('post_status.post') }}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -12,6 +12,7 @@ import InterfaceLanguageSwitcher from 'src/components/interface_language_switche
import BooleanSetting from '../helpers/boolean_setting.vue' import BooleanSetting from '../helpers/boolean_setting.vue'
import SharedComputedObject from '../helpers/shared_computed_object.js' import SharedComputedObject from '../helpers/shared_computed_object.js'
import localeService from 'src/services/locale/locale.service.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 { library } from '@fortawesome/fontawesome-svg-core'
import { import {
@ -46,9 +47,16 @@ const ProfileTab = {
emailLanguage: this.$store.state.users.currentUser.language || '', emailLanguage: this.$store.state.users.currentUser.language || '',
newPostTTLDays: this.$store.state.users.currentUser.status_ttl_days, newPostTTLDays: this.$store.state.users.currentUser.status_ttl_days,
expirePosts: this.$store.state.users.currentUser.status_ttl_days !== null, 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: { components: {
ChoiceSetting,
ScopeSelector, ScopeSelector,
ImageCropper, ImageCropper,
EmojiInput, EmojiInput,
@ -126,7 +134,8 @@ const ProfileTab = {
fields_attributes: this.newFields.filter(el => el != null), fields_attributes: this.newFields.filter(el => el != null),
bot: this.bot, bot: this.bot,
show_role: this.showRole, 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 */ /* eslint-enable camelcase */
} }

View file

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

View file

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

View file

@ -1,6 +1,6 @@
import { extractCommit } from 'src/services/version/version.service' 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 pleromaBeCommitUrl = 'https://akkoma.dev/AkkomaGang/akkoma/commit/'
const VersionTab = { const VersionTab = {

View file

@ -12,7 +12,7 @@
:key="group.role" :key="group.role"
class="staff-group" class="staff-group"
> >
<h4>{{ $t('general.role.' + group.role) }}</h4> <h4>The Admin Team</h4>
<basic-user-card <basic-user-card
v-for="user in group.users" v-for="user in group.users"
:key="user.screen_name" :key="user.screen_name"
@ -30,10 +30,21 @@
.staff-group { .staff-group {
padding-left: 1em; padding-left: 1em;
padding-top: 1em; padding-top: 0.2em;
padding-bottom: 2px;
.basic-user-card { .basic-user-card {
padding: 0.6em 0.4em;
padding-left: 0; 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, faLock,
faLockOpen, faLockOpen,
faGlobe, faGlobe,
faBiohazard,
faTimes, faTimes,
faRetweet, faRetweet,
faReply, faReply,
@ -46,6 +47,7 @@ library.add(
faEnvelope, faEnvelope,
faGlobe, faGlobe,
faLock, faLock,
faBiohazard,
faLockOpen, faLockOpen,
faTimes, faTimes,
faRetweet, faRetweet,
@ -455,7 +457,7 @@ const Status = {
case 'direct': case 'direct':
return 'envelope' return 'envelope'
case 'local': case 'local':
return 'users' return 'biohazard'
default: default:
return 'globe' return 'globe'
} }

View file

@ -193,6 +193,26 @@
:with-direction="true" :with-direction="true"
:auto-update="60" :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> </router-link>
<span <span
v-if="status.visibility" v-if="status.visibility"
@ -332,24 +352,6 @@
class="mentions-line" class="mentions-line"
/> />
</div> </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>
<div class="content"> <div class="content">

View file

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

View file

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

View file

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

View file

@ -235,7 +235,7 @@
line-height: 22px; line-height: 22px;
flex-wrap: wrap; flex-wrap: wrap;
.following, .requested_by { .following, .requested_by, .blocking {
flex: 1 0 auto; flex: 1 0 auto;
margin: 0; margin: 0;
margin-bottom: .25em; margin-bottom: .25em;

View file

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

View file

@ -10,11 +10,14 @@ import withLoadMore from '../../hocs/with_load_more/with_load_more'
import { debounce } from 'lodash' import { debounce } from 'lodash'
import { library } from '@fortawesome/fontawesome-svg-core' import { library } from '@fortawesome/fontawesome-svg-core'
import { import {
faCircleNotch faCircleNotch,
faCircleCheck
} from '@fortawesome/free-solid-svg-icons' } from '@fortawesome/free-solid-svg-icons'
import FollowedTagCard from '../followed_tag_card/FollowedTagCard.vue'
library.add( library.add(
faCircleNotch faCircleNotch,
faCircleCheck
) )
const FollowerList = withLoadMore({ const FollowerList = withLoadMore({
@ -33,6 +36,14 @@ const FriendList = withLoadMore({
additionalPropNames: ['userId'] additionalPropNames: ['userId']
})(List) })(List)
const FollowedTagList = withLoadMore({
fetch: (props, $store) => $store.dispatch('fetchFollowedTags', props.userId),
select: (props, $store) => get($store.getters.findUser(props.userId), 'followedTagIds', []).map(id => $store.getters.findTag(id)),
destroy: (props, $store) => $store.dispatch('clearFollowedTags', props.userId),
childPropName: 'items',
additionalPropNames: ['userId']
})(List)
const isUserPage = ({ name }) => name === 'user-profile' || name === 'external-user-profile' const isUserPage = ({ name }) => name === 'user-profile' || name === 'external-user-profile'
const UserProfile = { const UserProfile = {
@ -41,6 +52,7 @@ const UserProfile = {
error: false, error: false,
userId: null, userId: null,
tab: 'statuses', tab: 'statuses',
followsTab: 'users',
footerRef: null, footerRef: null,
note: null, note: null,
noteLoading: false noteLoading: false
@ -165,6 +177,9 @@ const UserProfile = {
this.tab = tab this.tab = tab
this.$router.replace({ hash: `#${tab}` }) this.$router.replace({ hash: `#${tab}` })
}, },
onFollowsTabSwitch (tab) {
this.followsTab = tab
},
linkClicked ({ target }) { linkClicked ({ target }) {
if (target.tagName === 'SPAN') { if (target.tagName === 'SPAN') {
target = target.parentNode target = target.parentNode
@ -200,6 +215,7 @@ const UserProfile = {
} }
}, },
components: { components: {
FollowedTagCard,
UserCard, UserCard,
Timeline, Timeline,
FollowerList, FollowerList,
@ -207,7 +223,8 @@ const UserProfile = {
FollowCard, FollowCard,
TabSwitcher, TabSwitcher,
Conversation, Conversation,
RichContent RichContent,
FollowedTagList
} }
} }

View file

@ -37,6 +37,15 @@
:html="field.value" :html="field.value"
:emoji="user.emoji" :emoji="user.emoji"
/> />
<span
v-if="field.verified_at"
class="user-profile-field-validated"
>
<FAIcon
icon="check-circle"
:title="$t('user_profile.field_validated')"
/>
</span>
</dd> </dd>
</dl> </dl>
</div> </div>
@ -90,27 +99,54 @@
:user-id="userId" :user-id="userId"
:in-profile="true" :in-profile="true"
:footer-slipgate="footerRef" :footer-slipgate="footerRef"
:disabled="!currentUser"
/> />
<div <div
v-if="followsTabVisible" v-if="followsTabVisible"
key="followees" key="followees"
:label="$t('user_card.followees')" :label="$t('user_card.followees')"
:disabled="!user.friends_count"
> >
<FriendList :user-id="userId"> <tab-switcher
<template v-slot:item="{item}"> :active-tab="followsTab"
<FollowCard :user="item" /> :render-only-focused="true"
</template> :on-switch="onFollowsTabSwitch"
</FriendList> >
<div
key="users"
:label="$t('user_card.followed_users')"
>
<FriendList :user-id="userId">
<template #item="{item}">
<FollowCard :user="item" />
</template>
</FriendList>
</div>
<div
key="tags"
v-if="isUs"
:label="$t('user_card.followed_tags')"
>
<FollowedTagList
:user-id="userId"
:get-key="(item) => item.name"
>
<template #item="{item}">
<FollowedTagCard :tag="item" />
</template>
<template #empty>
{{ $t('user_card.not_following_any_hashtags')}}
</template>
</FollowedTagList>
</div>
</tab-switcher>
</div> </div>
<div <div
v-if="followersTabVisible" v-if="followersTabVisible"
key="followers" key="followers"
:label="$t('user_card.followers')" :label="$t('user_card.followers')"
:disabled="!user.followers_count"
> >
<FollowerList :user-id="userId"> <FollowerList :user-id="userId">
<template v-slot:item="{item}"> <template #item="{item}">
<FollowCard <FollowCard
:user="item" :user="item"
:no-follows-you="isUs" :no-follows-you="isUs"
@ -128,6 +164,7 @@
:user-id="userId" :user-id="userId"
:in-profile="true" :in-profile="true"
:footer-slipgate="footerRef" :footer-slipgate="footerRef"
:disabled="!currentUser"
/> />
<Timeline <Timeline
v-if="isUs" v-if="isUs"
@ -225,6 +262,11 @@
padding: 0.5em 1.5em; padding: 0.5em 1.5em;
box-sizing: border-box; box-sizing: border-box;
} }
.user-profile-field-validated {
margin-left: 1rem;
color: green;
}
} }
} }

View file

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

View file

@ -1,11 +1,11 @@
{ {
"about": { "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", "bubble_instances_description": "Instances chosen by the admins to represent the local area of this instance",
"mrf": { "mrf": {
"federation": "Federation", "federation": "Federation",
"keyword": { "keyword": {
"ftl_removal": "Removal from \"The Whole Known Network\" Timeline", "ftl_removal": "Removal from Federated Timeline",
"is_replaced_by": "→", "is_replaced_by": "→",
"keyword_policies": "Keyword policies", "keyword_policies": "Keyword policies",
"reject": "Reject", "reject": "Reject",
@ -16,8 +16,8 @@
"simple": { "simple": {
"accept": "Accept", "accept": "Accept",
"accept_desc": "This instance only accepts messages from the following instances:", "accept_desc": "This instance only accepts messages from the following instances:",
"ftl_removal": "Removal from \"Known Network\" Timeline", "ftl_removal": "Removal from Federated Timeline",
"ftl_removal_desc": "This instance removes these instances from \"Known Network\" timeline:", "ftl_removal_desc": "This instance removes these instances from the federated timeline:",
"instance": "Instance", "instance": "Instance",
"media_nsfw": "Media force-set as sensitive", "media_nsfw": "Media force-set as sensitive",
"media_nsfw_desc": "This instance forces media to be set sensitive in posts on the following instances:", "media_nsfw_desc": "This instance forces media to be set sensitive in posts on the following instances:",
@ -27,8 +27,8 @@
"quarantine": "Quarantine", "quarantine": "Quarantine",
"quarantine_desc": "This instance will not send posts to the following instances:", "quarantine_desc": "This instance will not send posts to the following instances:",
"reason": "Reason", "reason": "Reason",
"reject": "Reject", "reject": "Instance Blocks",
"reject_desc": "This instance will not accept messages from the following instances:", "reject_desc": "This instance blocks messages to and from the following instances:",
"simple_policies": "Instance-specific policies" "simple_policies": "Instance-specific policies"
} }
}, },
@ -86,7 +86,8 @@
"load_all_hint": "Loaded first {saneAmount} emoji, loading all emoji may cause performance issues.", "load_all_hint": "Loaded first {saneAmount} emoji, loading all emoji may cause performance issues.",
"search_emoji": "Search for an emoji", "search_emoji": "Search for an emoji",
"stickers": "Stickers", "stickers": "Stickers",
"unicode": "Unicode emoji" "unicode": "Unicode emoji",
"recent": "Recently used"
}, },
"errors": { "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." "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", "announcements": "Announcements",
"back": "Back", "back": "Back",
"bookmarks": "Bookmarks", "bookmarks": "Bookmarks",
"bubble_timeline": "Bubble timeline", "bubble_timeline": "Recommended Instances",
"bubble_timeline_description": "Posts from instances close to yours, as recommended by the admins", "bubble_timeline_description": "Posts from instances close to yours, as recommended by the admins",
"chats": "Chats", "chats": "Chats",
"dms": "Direct messages", "dms": "Direct Messages",
"friend_requests": "Follow requests", "friend_requests": "Follow Requests",
"home_timeline": "Home timeline", "home_timeline": "Home Timeline",
"home_timeline_description": "Posts from people you follow", "home_timeline_description": "Posts from people you follow",
"interactions": "Interactions", "interactions": "Interactions",
"lists": "Lists", "lists": "Lists",
@ -315,11 +316,11 @@
"moderation": "Moderation", "moderation": "Moderation",
"preferences": "Preferences", "preferences": "Preferences",
"public_timeline_description": "Public posts from this instance", "public_timeline_description": "Public posts from this instance",
"public_tl": "Public timeline", "public_tl": "Local Timeline",
"search": "Search", "search": "Search",
"timeline": "Timeline", "timeline": "Timeline",
"timelines": "Timelines", "timelines": "Timelines",
"twkn": "Known Network", "twkn": "Federated Timeline",
"twkn_timeline_description": "Posts from the entire network", "twkn_timeline_description": "Posts from the entire network",
"user_search": "User Search", "user_search": "User Search",
"who_to_follow": "Who to follow" "who_to_follow": "Who to follow"
@ -379,7 +380,7 @@
"text/x.misskeymarkdown": "MFM" "text/x.misskeymarkdown": "MFM"
}, },
"content_warning": "Content Warning (optional)", "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_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.", "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!", "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_profile_default_tab": "Default Tab on User Profile",
"user_profiles": "User Profiles", "user_profiles": "User Profiles",
"user_settings": "User Settings", "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", "valid_until": "Valid until",
"values": { "values": {
"false": "no", "false": "no",
@ -963,7 +968,7 @@
"edit": "Edit", "edit": "Edit",
"edit_history": "Edit History", "edit_history": "Edit History",
"edit_history_modal_title": "Edited {historyCount} time | Edited {historyCount} times", "edit_history_modal_title": "Edited {historyCount} time | Edited {historyCount} times",
"edited_at": "Edited {time}", "edited_at": " (Edited {time})",
"expand": "Expand", "expand": "Expand",
"external_source": "External source", "external_source": "External source",
"favorites": "Favorites", "favorites": "Favorites",
@ -1056,6 +1061,7 @@
"show_new": "Show new", "show_new": "Show new",
"socket_broke": "Realtime connection lost: CloseEvent code {0}", "socket_broke": "Realtime connection lost: CloseEvent code {0}",
"socket_reconnected": "Realtime connection established", "socket_reconnected": "Realtime connection established",
"follow_tag": "Follow hashtag",
"unfollow_tag": "Unfollow hashtag", "unfollow_tag": "Unfollow hashtag",
"up_to_date": "Up-to-date" "up_to_date": "Up-to-date"
}, },
@ -1121,6 +1127,7 @@
"block_confirm_title": "Block user", "block_confirm_title": "Block user",
"block_progress": "Blocking…", "block_progress": "Blocking…",
"blocked": "Blocked!", "blocked": "Blocked!",
"blocks_you": "Blocks you!",
"bot": "Bot", "bot": "Bot",
"deactivated": "Deactivated", "deactivated": "Deactivated",
"deny": "Deny", "deny": "Deny",
@ -1138,6 +1145,8 @@
"follow_unfollow": "Unfollow", "follow_unfollow": "Unfollow",
"followees": "Following", "followees": "Following",
"followers": "Followers", "followers": "Followers",
"followed_tags": "Followed hashtags",
"followed_users": "Followed users",
"following": "Following!", "following": "Following!",
"follows_you": "Follows you!", "follows_you": "Follows you!",
"hidden": "Hidden", "hidden": "Hidden",
@ -1176,6 +1185,9 @@
"unfollow_confirm_accept_button": "Yes, unfollow", "unfollow_confirm_accept_button": "Yes, unfollow",
"unfollow_confirm_cancel_button": "No, don't unfollow", "unfollow_confirm_cancel_button": "No, don't unfollow",
"unfollow_confirm_title": "Unfollow user", "unfollow_confirm_title": "Unfollow user",
"not_following_any_hashtags": "You are not following any hashtags",
"follow_tag": "Follow hashtag",
"unfollow_tag": "Unfollow hashtag",
"unmute": "Unmute", "unmute": "Unmute",
"unmute_progress": "Unmuting…", "unmute_progress": "Unmuting…",
"unsubscribe": "Unsubscribe" "unsubscribe": "Unsubscribe"
@ -1183,7 +1195,8 @@
"user_profile": { "user_profile": {
"profile_does_not_exist": "Sorry, this profile does not exist.", "profile_does_not_exist": "Sorry, this profile does not exist.",
"profile_loading_error": "Sorry, there was an error loading this profile.", "profile_loading_error": "Sorry, there was an error loading this profile.",
"timeline_title": "User timeline" "timeline_title": "User timeline",
"field_validated": "Link Verified"
}, },
"user_reporting": { "user_reporting": {
"add_comment_description": "The report will be sent to your instance moderators. You can provide an explanation of why you are reporting this account below:", "add_comment_description": "The report will be sent to your instance moderators. You can provide an explanation of why you are reporting this account below:",

View file

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

View file

@ -22,6 +22,7 @@ import announcementsModule from './modules/announcements.js'
import editStatusModule from './modules/editStatus.js' import editStatusModule from './modules/editStatus.js'
import statusHistoryModule from './modules/statusHistory.js' import statusHistoryModule from './modules/statusHistory.js'
import tagModule from './modules/tags.js' import tagModule from './modules/tags.js'
import recentEmojisModule from './modules/recentEmojis.js'
import { createI18n } from 'vue-i18n' import { createI18n } from 'vue-i18n'
@ -47,7 +48,8 @@ const persistedStateOptions = {
paths: [ paths: [
'config', 'config',
'users.lastLoginName', 'users.lastLoginName',
'oauth' 'oauth',
'recentEmojis.emojis',
] ]
}; };
@ -98,7 +100,8 @@ const persistedStateOptions = {
announcements: announcementsModule, announcements: announcementsModule,
editStatus: editStatusModule, editStatus: editStatusModule,
statusHistory: statusHistoryModule, statusHistory: statusHistoryModule,
tags: tagModule tags: tagModule,
recentEmojis: recentEmojisModule,
}, },
plugins, plugins,
strict: false // Socket modifies itself, let's ignore this for now. 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 backendInteractorService from '../services/backend_interactor_service/backend_interactor_service.js'
import { WSConnectionStatus } from '../services/api/api.service.js' import { WSConnectionStatus } from '../services/api/api.service.js'
import { map } from 'lodash'
const retryTimeout = (multiplier) => 1000 * multiplier const retryTimeout = (multiplier) => 1000 * multiplier
@ -40,9 +41,6 @@ const api = {
setSocket (state, socket) { setSocket (state, socket) {
state.socket = socket state.socket = socket
}, },
setFollowRequests (state, value) {
state.followRequests = value
},
setMastoUserSocketStatus (state, value) { setMastoUserSocketStatus (state, value) {
state.mastoUserSocketStatus = value state.mastoUserSocketStatus = value
}, },
@ -51,6 +49,15 @@ const api = {
}, },
resetRetryMultiplier (state) { resetRetryMultiplier (state) {
state.retryMultiplier = 1 state.retryMultiplier = 1
},
setFollowRequests (state, value) {
state.followRequests = [...value]
},
saveFollowRequests (state, requests) {
state.followRequests = [...state.followRequests, ...requests]
},
saveFollowRequestPagination (state, pagination) {
state.followRequestsPagination = pagination
} }
}, },
actions: { actions: {
@ -240,24 +247,22 @@ const api = {
...rest ...rest
}) })
}, },
// Follow requests
startFetchingFollowRequests (store) {
if (store.state.fetchers['followRequests']) return
const fetcher = store.state.backendInteractor.startFetchingFollowRequests({ store })
store.commit('addFetcher', { fetcherName: 'followRequests', fetcher })
},
stopFetchingFollowRequests (store) {
const fetcher = store.state.fetchers.followRequests
if (!fetcher) return
store.commit('removeFetcher', { fetcherName: 'followRequests', fetcher })
},
removeFollowRequest (store, request) { removeFollowRequest (store, request) {
let requests = store.state.followRequests.filter((it) => it !== request) let requests = [...store.state.followRequests].filter((it) => it.id !== request.id)
store.commit('setFollowRequests', requests) store.commit('setFollowRequests', requests)
}, },
fetchFollowRequests ({ rootState, commit }) {
const pagination = rootState.api.followRequestsPagination
return rootState.api.backendInteractor.getFollowRequests({ pagination })
.then((requests) => {
if (requests.data.length > 0) {
commit('addNewUsers', requests.data)
commit('saveFollowRequests', requests.data)
commit('saveFollowRequestPagination', requests.pagination)
}
return requests
})
},
// Lists // Lists
startFetchingLists (store) { startFetchingLists (store) {
if (store.state.fetchers['lists']) return if (store.state.fetchers['lists']) return

View file

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

View file

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

View file

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

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

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' import { registerPushNotifications, unregisterPushNotifications } from '../services/push/push.js'
// TODO: Unify with mergeOrAdd in statuses.js // TODO: Unify with mergeOrAdd in statuses.js
export const mergeOrAdd = (arr, obj, item) => { export const mergeOrAdd = (arr, obj, item, key = 'id') => {
if (!item) { return false } if (!item) { return false }
const oldItem = obj[item.id] const oldItem = obj[item[key]]
if (oldItem) { if (oldItem) {
// We already have this, so only merge the new info. // We already have this, so only merge the new info.
mergeWith(oldItem, item, mergeArrayLength) mergeWith(oldItem, item, mergeArrayLength)
@ -15,7 +15,7 @@ export const mergeOrAdd = (arr, obj, item) => {
} else { } else {
// This is a new item, prepare it // This is a new item, prepare it
arr.push(item) arr.push(item)
obj[item.id] = item obj[item[key]] = item
if (item.screen_name && !item.screen_name.includes('@')) { if (item.screen_name && !item.screen_name.includes('@')) {
obj[item.screen_name.toLowerCase()] = item obj[item.screen_name.toLowerCase()] = item
} }
@ -157,6 +157,14 @@ export const mutations = {
const user = state.usersObject[id] const user = state.usersObject[id]
user.followerIds = uniq(concat(user.followerIds || [], followerIds)) user.followerIds = uniq(concat(user.followerIds || [], followerIds))
}, },
saveFollowedTagIds (state, { id, followedTagIds }) {
const user = state.usersObject[id]
user.followedTagIds = uniq(concat(user.followedTagIds || [], followedTagIds))
},
saveFollowedTagPagination (state, { id, pagination }) {
const user = state.usersObject[id]
user.followedTagPagination = pagination
},
// Because frontend doesn't have a reason to keep these stuff in memory // Because frontend doesn't have a reason to keep these stuff in memory
// outside of viewing someones user profile. // outside of viewing someones user profile.
clearFriends (state, userId) { clearFriends (state, userId) {
@ -171,6 +179,12 @@ export const mutations = {
user['followerIds'] = [] user['followerIds'] = []
} }
}, },
clearFollowedTags (state, userId) {
const user = state.usersObject[userId]
if (user) {
user['followedTagIds'] = []
}
},
addNewUsers (state, users) { addNewUsers (state, users) {
each(users, (user) => { each(users, (user) => {
if (user.relationship) { if (user.relationship) {
@ -251,6 +265,12 @@ export const mutations = {
signUpFailure (state, errors) { signUpFailure (state, errors) {
state.signUpPending = false state.signUpPending = false
state.signUpErrors = errors state.signUpErrors = errors
},
decrementFollowRequestsCount (store) {
store.currentUser.follow_requests_count--
},
incrementFollowRequestsCount (store) {
store.currentUser.follow_requests_count++
} }
} }
@ -271,7 +291,7 @@ export const getters = {
relationship: state => id => { relationship: state => id => {
const rel = id && state.relationships[id] const rel = id && state.relationships[id]
return rel || { id, loading: true } return rel || { id, loading: true }
} },
} }
export const defaultState = { export const defaultState = {
@ -282,7 +302,9 @@ export const defaultState = {
usersObject: {}, usersObject: {},
signUpPending: false, signUpPending: false,
signUpErrors: [], signUpErrors: [],
relationships: {} relationships: {},
tags: [],
tagsObject: {}
} }
const users = { const users = {
@ -402,12 +424,27 @@ const users = {
return followers return followers
}) })
}, },
fetchFollowedTags ({ rootState, commit }, id) {
const user = rootState.users.usersObject[id]
const pagination = user.followedTagPagination
return rootState.api.backendInteractor.getFollowedHashtags({ pagination })
.then(({ data: tags, pagination }) => {
each(tags, tag => commit('setTag', { name: tag.name, data: tag }))
commit('saveFollowedTagIds', { id, followedTagIds: tags.map(tag => tag.name) })
commit('saveFollowedTagPagination', { id, pagination })
return tags
})
},
clearFriends ({ commit }, userId) { clearFriends ({ commit }, userId) {
commit('clearFriends', userId) commit('clearFriends', userId)
}, },
clearFollowers ({ commit }, userId) { clearFollowers ({ commit }, userId) {
commit('clearFollowers', userId) commit('clearFollowers', userId)
}, },
clearFollowedTags ({ commit }, userId) {
commit('clearFollowedTags', userId)
},
subscribeUser ({ rootState, commit }, id) { subscribeUser ({ rootState, commit }, id) {
return rootState.api.backendInteractor.subscribeUser({ id }) return rootState.api.backendInteractor.subscribeUser({ id })
.then((relationship) => commit('updateUserRelationship', [relationship])) .then((relationship) => commit('updateUserRelationship', [relationship]))
@ -473,6 +510,12 @@ const users = {
store.commit('setUserForNotification', notification) store.commit('setUserForNotification', notification)
}) })
}, },
decrementFollowRequestsCount (store) {
store.commit('decrementFollowRequestsCount')
},
incrementFollowRequestsCount (store) {
store.commit('incrementFollowRequestsCount')
},
searchUsers ({ rootState, commit }, { query }) { searchUsers ({ rootState, commit }, { query }) {
return rootState.api.backendInteractor.searchUsers({ query }) return rootState.api.backendInteractor.searchUsers({ query })
.then((users) => { .then((users) => {
@ -536,7 +579,6 @@ const users = {
store.dispatch('stopFetchingTimeline', 'friends') store.dispatch('stopFetchingTimeline', 'friends')
store.commit('setBackendInteractor', backendInteractorService(store.getters.getToken())) store.commit('setBackendInteractor', backendInteractorService(store.getters.getToken()))
store.dispatch('stopFetchingNotifications') store.dispatch('stopFetchingNotifications')
store.dispatch('stopFetchingFollowRequests')
store.dispatch('stopFetchingConfig') store.dispatch('stopFetchingConfig')
store.commit('clearNotifications') store.commit('clearNotifications')
store.commit('resetStatuses') store.commit('resetStatuses')
@ -595,13 +637,16 @@ const users = {
// Get user mutes // Get user mutes
store.dispatch('fetchMutes') store.dispatch('fetchMutes')
store.dispatch('setLayoutWidth', windowWidth()) store.dispatch('setLayoutWidth', windowWidth())
store.dispatch('setLayoutHeight', windowHeight()) store.dispatch('setLayoutHeight', windowHeight())
store.dispatch('getSupportedTranslationlanguages') store.dispatch('getSupportedTranslationlanguages')
store.dispatch('getSettingsProfile') store.dispatch('getSettingsProfile')
store.dispatch('listSettingsProfiles') store.dispatch('listSettingsProfiles')
store.dispatch('startFetchingConfig') store.dispatch('startFetchingConfig')
store.dispatch('startFetchingAnnouncements')
if (user.role === 'admin' || user.role === 'moderator') {
store.dispatch('startFetchingReports')
}
// Fetch our friends // Fetch our friends
store.rootState.api.backendInteractor.fetchFriends({ id: user.id }) store.rootState.api.backendInteractor.fetchFriends({ id: user.id })

View file

@ -1,6 +1,7 @@
import { each, map, concat, last, get } from 'lodash' import { each, map, concat, last, get } from 'lodash'
import { parseStatus, parseSource, parseUser, parseNotification, parseReport, parseAttachment, parseLinkHeaderPagination } from '../entity_normalizer/entity_normalizer.service.js' import { parseStatus, parseSource, parseUser, parseNotification, parseReport, parseAttachment, parseLinkHeaderPagination } from '../entity_normalizer/entity_normalizer.service.js'
import { RegistrationError, StatusCodeError } from '../errors/errors' import { RegistrationError, StatusCodeError } from '../errors/errors'
import { Url } from 'url'
/* eslint-env browser */ /* eslint-env browser */
const MUTES_IMPORT_URL = '/api/pleroma/mutes_import' const MUTES_IMPORT_URL = '/api/pleroma/mutes_import'
@ -111,6 +112,7 @@ const AKKOMA_SETTING_PROFILE_LIST = `/api/v1/akkoma/frontend_settings/pleroma-fe
const MASTODON_TAG_URL = (name) => `/api/v1/tags/${name}` const MASTODON_TAG_URL = (name) => `/api/v1/tags/${name}`
const MASTODON_FOLLOW_TAG_URL = (name) => `/api/v1/tags/${name}/follow` const MASTODON_FOLLOW_TAG_URL = (name) => `/api/v1/tags/${name}/follow`
const MASTODON_UNFOLLOW_TAG_URL = (name) => `/api/v1/tags/${name}/unfollow` const MASTODON_UNFOLLOW_TAG_URL = (name) => `/api/v1/tags/${name}/unfollow`
const MASTODON_FOLLOWED_TAGS_URL = '/api/v1/followed_tags'
const oldfetch = window.fetch const oldfetch = window.fetch
@ -404,14 +406,6 @@ const fetchFollowers = ({ id, maxId, sinceId, limit = 20, credentials }) => {
.then((data) => data.json()) .then((data) => data.json())
.then((data) => data.map(parseUser)) .then((data) => data.map(parseUser))
} }
const fetchFollowRequests = ({ credentials }) => {
const url = MASTODON_FOLLOW_REQUESTS_URL
return fetch(url, { headers: authHeaders(credentials) })
.then((data) => data.json())
.then((data) => data.map(parseUser))
}
const fetchLists = ({ credentials }) => { const fetchLists = ({ credentials }) => {
const url = MASTODON_LISTS_URL const url = MASTODON_LISTS_URL
return fetch(url, { headers: authHeaders(credentials) }) return fetch(url, { headers: authHeaders(credentials) })
@ -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 = {} }) => { export const getMastodonSocketURI = ({ credentials, stream, args = {} }) => {
return Object.entries({ return Object.entries({
...(credentials ...(credentials
@ -1764,7 +1800,6 @@ const apiService = {
mfaConfirmOTP, mfaConfirmOTP,
addBackup, addBackup,
listBackups, listBackups,
fetchFollowRequests,
fetchLists, fetchLists,
createList, createList,
getList, getList,
@ -1813,7 +1848,9 @@ const apiService = {
deleteNoteFromReport, deleteNoteFromReport,
getHashtag, getHashtag,
followHashtag, followHashtag,
unfollowHashtag unfollowHashtag,
getFollowedHashtags,
getFollowRequests
} }
export default apiService export default apiService

View file

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

View file

@ -68,13 +68,15 @@ export const parseUser = (data) => {
output.fields_html = data.fields.map(field => { output.fields_html = data.fields.map(field => {
return { return {
name: escape(field.name), name: escape(field.name),
value: field.value value: field.value,
verified_at: field.verified_at
} }
}) })
output.fields_text = data.fields.map(field => { output.fields_text = data.fields.map(field => {
return { return {
name: unescape(field.name.replace(/<[^>]*>/g, '')), name: unescape(field.name.replace(/<[^>]*>/g, '')),
value: unescape(field.value.replace(/<[^>]*>/g, '')) value: unescape(field.value.replace(/<[^>]*>/g, '')),
verified_at: field.verified_at
} }
}) })
@ -88,6 +90,8 @@ export const parseUser = (data) => {
output.friends_count = data.following_count output.friends_count = data.following_count
output.bot = data.bot output.bot = data.bot
output.accepts_direct_messages_from = data.accepts_direct_messages_from
output.follow_requests_count = data.follow_requests_count
if (data.akkoma) { if (data.akkoma) {
output.instance = data.akkoma.instance output.instance = data.akkoma.instance
output.status_ttl_days = data.akkoma.status_ttl_days output.status_ttl_days = data.akkoma.status_ttl_days
@ -408,8 +412,10 @@ export const parseNotification = (data) => {
if (masto) { if (masto) {
output.type = mastoDict[data.type] || data.type output.type = mastoDict[data.type] || data.type
output.seen = data.pleroma.is_seen output.seen = data.pleroma.is_seen
output.status = isStatusNotification(output.type) ? parseStatus(data.status) : null if (data.status) {
output.action = output.status // TODO: Refactor, this is unneeded output.status = isStatusNotification(output.type) ? parseStatus(data.status) : null
output.action = output.status // TODO: Refactor, this is unneeded
}
output.target = output.type !== 'move' output.target = output.type !== 'move'
? null ? null
: parseUser(data.target) : parseUser(data.target)

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) => { export const applyTheme = (input) => {
const { rules } = generatePreset(input) const { rules } = generatePreset(input)
const head = document.head
const body = document.body const body = document.body
body.classList.add('hidden') body.classList.add('hidden')
const styleEl = document.createElement('style') /** @type {CSSStyleSheet} */
head.appendChild(styleEl) const styleSheet = document.getElementById('theme-holder').sheet
const styleSheet = styleEl.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') 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" minimatch "^3.0.4"
strip-json-comments "^3.1.1" strip-json-comments "^3.1.1"
"@floatingghost/pinch-zoom-element@^1.3.1":
version "1.3.1"
resolved "https://registry.yarnpkg.com/@floatingghost/pinch-zoom-element/-/pinch-zoom-element-1.3.1.tgz#5f327ad17ddf1f56777098aca088fdbf99cbd049"
integrity sha512-KnE7aBQdd/Fj1TzU5uzgwD9YAQ58DTMUks/PoTEBFW4zi0lBM9cN/j45wzcnzsT2VXG1S6qM7NMmq7NGm2//Fg==
dependencies:
pointer-tracker "^2.0.3"
"@fortawesome/fontawesome-common-types@6.2.0": "@fortawesome/fontawesome-common-types@6.2.0":
version "6.2.0" version "6.2.0"
resolved "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.2.0.tgz" resolved "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.2.0.tgz"
@ -1516,13 +1523,6 @@
"@jridgewell/resolve-uri" "^3.0.3" "@jridgewell/resolve-uri" "^3.0.3"
"@jridgewell/sourcemap-codec" "^1.4.10" "@jridgewell/sourcemap-codec" "^1.4.10"
"@kazvmoe-infra/pinch-zoom-element@1.2.0":
version "1.2.0"
resolved "https://registry.npmjs.org/@kazvmoe-infra/pinch-zoom-element/-/pinch-zoom-element-1.2.0.tgz"
integrity sha512-HBrhH5O/Fsp2bB7EGTXzCsBAVcMjknSagKC5pBdGpKsF8meHISR0kjDIdw4YoE0S+0oNMwJ6ZUZyIBrdywxPPw==
dependencies:
pointer-tracker "^2.0.3"
"@nicolo-ribaudo/eslint-scope-5-internals@5.1.1-v1": "@nicolo-ribaudo/eslint-scope-5-internals@5.1.1-v1":
version "5.1.1-v1" version "5.1.1-v1"
resolved "https://registry.yarnpkg.com/@nicolo-ribaudo/eslint-scope-5-internals/-/eslint-scope-5-internals-5.1.1-v1.tgz#dbf733a965ca47b1973177dc0bb6c889edcfb129" resolved "https://registry.yarnpkg.com/@nicolo-ribaudo/eslint-scope-5-internals/-/eslint-scope-5-internals-5.1.1-v1.tgz#dbf733a965ca47b1973177dc0bb6c889edcfb129"