Compare commits

...

24 commits

Author SHA1 Message Date
4919f32ce4 Merge branch 'develop' into fedi-absturztau-be 2023-01-21 09:12:47 +01:00
56fd2e773b Merge branch 'languages' into develop 2023-01-15 17:59:50 +00:00
42dc1a027a add language input 2023-01-15 17:59:32 +00:00
236bc2c762 Merge pull request 'Only show "keep open" emoji checkbox on post form' (#269) from sfr/pleroma-fe:fix/keepopen into develop
Reviewed-on: AkkomaGang/akkoma-fe#269
2023-01-09 22:20:31 +00:00
Sol Fisher Romanoff
e9f47509ae
Only show "keep open" emoji checkbox on post form 2023-01-03 16:04:26 +02:00
f288d0c219 Make everything work with a strict CSP 2023-01-02 15:16:42 +00:00
d973396c96 Remove console.log 2023-01-01 21:06:02 +00:00
62287fffae add follow/unfollow to followed tags list 2023-01-01 21:05:25 +00:00
e9f16af82d Add list of followed hashtags to profile 2023-01-01 20:11:07 +00:00
dfba8be134 Fall back to nsfw image if no blurhash 2022-12-30 05:03:25 +00:00
313ddcebcb Add blurhash support 2022-12-30 04:57:23 +00:00
236b19e854 Merge branch 'develop' of akkoma.dev:AkkomaGang/akkoma-fe into develop 2022-12-30 03:20:30 +00:00
ea941d7cfa remove IHBA assets 2022-12-30 03:20:12 +00:00
2e5001e5de Allow follow(er) lists to be acessible by account owner even if follower counts are disabled (#246)
Currently, if a user has their follower/follow counts hidden, they cannot access their own list of followers/follows. This makes no real sense and means that they cannot modify those lists without disabling their privacy options.

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

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

Closes: AkkomaGang/pleroma-fe#255

Co-authored-by: Francis Dinh <normandy@biribiri.dev>
Reviewed-on: AkkomaGang/akkoma-fe#256
Co-authored-by: Norm <normandy@biribiri.dev>
Co-committed-by: Norm <normandy@biribiri.dev>
2022-12-30 03:01:17 +00:00
dd403b295f Merge pull request 'Remove stray debug log' (#265) from sfr/pleroma-fe:del-log into develop
Reviewed-on: AkkomaGang/akkoma-fe#265
2022-12-30 03:00:49 +00:00
Sol Fisher Romanoff
9cd62fe08d
Remove stray debug log 2022-12-30 01:03:31 +02:00
f668455dff Merge branch 'link-verification' into develop 2022-12-29 20:56:22 +00:00
5a4315384e force CI build 2022-12-29 15:25:03 +00:00
401dfa8fa6 update readme 2022-12-29 15:22:06 +00:00
bb243168b3 Revert "Merge pull request 'Don't show timeline links if disabled and logged out' (#250) from sfr/pleroma-fe:fix/hide-timelines into develop"
This reverts commit 0b5793c1e0, reversing
changes made to 72ef2e7454.
2022-12-29 15:18:13 +00:00
da491f3278 add verification of links 2022-12-29 15:17:35 +00:00
d00e28d5e9 fix emoji picker in replies in notifications 2022-12-22 05:43:01 +00:00
7ff17ab722 don't crash out if notification status is null 2022-12-20 13:20:13 +00:00
39 changed files with 431 additions and 119 deletions

View file

@ -1,8 +1,8 @@
# 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
@ -21,15 +21,15 @@ This is a fork of Pleroma-FE from the Pleroma project, with support for new Akko
# 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

View file

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

View file

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

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;

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'

View file

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

View file

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

View file

@ -0,0 +1,66 @@
<template>
<canvas
ref="canvas"
class="blurhash"
/>
</template>
<script>
import { decode } from "blurhash";
export default {
name: 'Blurhash',
props: {
hash: {
type: String,
required: true,
},
width: {
type: Number,
required: true,
},
height: {
type: Number,
required: true,
},
punch: {
type: Number,
default: null,
},
},
data() {
return {
canvas: null,
ctx: null,
};
},
mounted() {
this.canvas = this.$refs.canvas;
this.ctx = this.canvas.getContext('2d');
this.canvas.width = 1024;
this.canvas.height = 512;
this.draw();
},
methods: {
draw() {
const pixels = decode(this.hash, this.width, this.height, this.punch);
const imageData = this.ctx.createImageData(this.width, this.height);
imageData.data.set(pixels);
this.ctx.putImageData(imageData, 0, 0);
fetch("/static/blurhash-overlay.png")
.then((response) => response.blob())
.then((blob) => {
const img = new Image();
img.src = URL.createObjectURL(blob);
img.onload = () => {
this.ctx.drawImage(img, 0, 0, this.width, this.height);
};
});
},
}
}
</script>
<style scoped>
</style>

View file

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

@ -44,7 +44,6 @@
/> />
</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"
> >
@ -68,7 +67,6 @@
/> />
</router-link> </router-link>
<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"
> >

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"

View file

@ -27,6 +27,11 @@ const EmojiPicker = {
required: false, required: false,
type: Boolean, type: Boolean,
default: false default: false
},
showKeepOpen: {
required: false,
type: Boolean,
default: false
} }
}, },
data () { data () {

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 {

View file

@ -84,7 +84,10 @@
<span :ref="'group-end-' + group.id" /> <span :ref="'group-end-' + group.id" />
</div> </div>
</div> </div>
<div class="keep-open"> <div
v-if="showKeepOpen"
class="keep-open"
>
<Checkbox v-model="keepOpen"> <Checkbox v-model="keepOpen">
{{ $t('emoji.keep_open') }} {{ $t('emoji.keep_open') }}
</Checkbox> </Checkbox>

View file

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

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

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

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

View file

@ -194,6 +194,23 @@
:on-scope-change="changeVis" :on-scope-change="changeVis"
/> />
<div
class="language-selector"
>
<Select
id="post-language"
v-model="newStatus.language"
class="form-control"
>
<option
v-for="language in isoLanguages"
:key="language"
:value="language"
>
{{ language }}
</option>
</Select>
</div>
<div <div
v-if="postFormats.length > 1" v-if="postFormats.length > 1"
class="text-format" class="text-format"

View file

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

View file

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

@ -32,7 +32,7 @@
>{{ $t("nav.bubble_timeline") }}</span> >{{ $t("nav.bubble_timeline") }}</span>
</router-link> </router-link>
</li> </li>
<li v-if="currentUser || !(privateMode || restrictedTimelines.public)"> <li v-if="currentUser || !privateMode">
<router-link <router-link
class="menu-item" class="menu-item"
:to="{ name: 'public-timeline' }" :to="{ name: 'public-timeline' }"
@ -48,7 +48,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' }"

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

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
}) })

View file

@ -939,6 +939,7 @@
"title": "Version" "title": "Version"
}, },
"virtual_scrolling": "Optimize timeline rendering", "virtual_scrolling": "Optimize timeline rendering",
"use_blurhash": "Use blurhashes for NSFW thumbnails",
"word_filter": "Word filter", "word_filter": "Word filter",
"wordfilter": "Wordfilter" "wordfilter": "Wordfilter"
}, },
@ -1056,6 +1057,7 @@
"show_new": "Show new", "show_new": "Show new",
"socket_broke": "Realtime connection lost: CloseEvent code {0}", "socket_broke": "Realtime connection lost: CloseEvent code {0}",
"socket_reconnected": "Realtime connection established", "socket_reconnected": "Realtime connection established",
"follow_tag": "Follow hashtag",
"unfollow_tag": "Unfollow hashtag", "unfollow_tag": "Unfollow hashtag",
"up_to_date": "Up-to-date" "up_to_date": "Up-to-date"
}, },
@ -1138,6 +1140,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 +1180,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 +1190,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

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

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

@ -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) {
@ -277,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 = {
@ -288,7 +302,9 @@ export const defaultState = {
usersObject: {}, usersObject: {},
signUpPending: false, signUpPending: false,
signUpErrors: [], signUpErrors: [],
relationships: {} relationships: {},
tags: [],
tagsObject: {}
} }
const users = { const users = {
@ -408,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]))

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
@ -878,7 +880,8 @@ const postStatus = ({
quoteId, quoteId,
contentType, contentType,
preview, preview,
idempotencyKey idempotencyKey,
language
}) => { }) => {
const form = new FormData() const form = new FormData()
const pollOptions = poll.options || [] const pollOptions = poll.options || []
@ -889,6 +892,7 @@ const postStatus = ({
if (visibility) form.append('visibility', visibility) if (visibility) form.append('visibility', visibility)
if (sensitive) form.append('sensitive', sensitive) if (sensitive) form.append('sensitive', sensitive)
if (contentType) form.append('content_type', contentType) if (contentType) form.append('content_type', contentType)
if (language) form.append('language', language)
mediaIds.forEach(val => { mediaIds.forEach(val => {
form.append('media_ids[]', val) form.append('media_ids[]', val)
}) })
@ -1575,6 +1579,28 @@ 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
}
});
}
export const getMastodonSocketURI = ({ credentials, stream, args = {} }) => { export const getMastodonSocketURI = ({ credentials, stream, args = {} }) => {
return Object.entries({ return Object.entries({
...(credentials ...(credentials
@ -1813,7 +1839,8 @@ const apiService = {
deleteNoteFromReport, deleteNoteFromReport,
getHashtag, getHashtag,
followHashtag, followHashtag,
unfollowHashtag unfollowHashtag,
getFollowedHashtags,
} }
export default apiService export default apiService

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
} }
}) })
@ -232,13 +234,14 @@ export const parseAttachment = (data) => {
if (masto) { if (masto) {
// Not exactly same... // Not exactly same...
output.mimetype = data.pleroma ? data.pleroma.mime_type : data.type output.mimetype = data.pleroma ? data.pleroma.mime_type : data.type
output.meta = data.meta // not present in BE yet output.meta = data.meta
output.id = data.id output.id = data.id
} else { } else {
output.mimetype = data.mimetype output.mimetype = data.mimetype
// output.meta = ??? missing // output.meta = ??? missing
} }
output.blurhash = data.blurhash
output.url = data.url output.url = data.url
output.large_thumb_url = data.preview_url output.large_thumb_url = data.preview_url
output.description = data.description output.description = data.description
@ -408,8 +411,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
if (data.status) {
output.status = isStatusNotification(output.type) ? parseStatus(data.status) : null output.status = isStatusNotification(output.type) ? parseStatus(data.status) : null
output.action = output.status // TODO: Refactor, this is unneeded 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

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

View file

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

View file

@ -1,45 +0,0 @@
<h4>Terms of Service</h4>
<p>It's mainly "be nice"</p>
<ol>
<li>
<h3>Don't be a big meanie</h4>
<p>Arguments are cool and all but don't make them into flamewars. Try to act in good faith - we want to be at least on good terms with people. Please act with understanding towards others on this instance. Most people here are probably struggling with a lot, be mindful of that.</p>
</li>
<li>
<h3>Mark your lewds!</h3>
<p>Reminder that lewd is bad and nobody wants to be forced to see that. Just mark it sensitive, and post unlisted. That is to say, anything suggestive/ecchi upwards should be marked. If you wouldn't look at it with your parents/boss in the room, mark it. It goes without saying that if you're <em>going</em> to post lewd stuff, keep it sensible. Obviously nothing underaged or otherwise questionable. Or you could just not post lewd stuff. Either/or.</p>
</li>
<li>
<h3>This is a <b>Kink Shame Zone</b></h3>
<p>Being a lewdie will be met with many anime girl reaction images shaming you for your lewdness. Go think about icky things on someone else's webzone™</p>
</li>
<li>
<h3>Keep it legal!</h3>
<p>Server is hosted in france, keep content legal for there (+ wherever you're browsing from)</p>
</li>
<li>
<h3>No ads/spambots</h3>
<p>I didn't think I'd have to specify this, but please do not set up bots solely for trying to advertise.</h3>
</li>
<li>
<h3>Non-TOS recommendations</h3>
<p>This is stuff that'd I'd <em>like</em> you to do, but I won't outright ban you if you don't follow them</p>
<ul>
<li>If someone is sadposting, don't antagonise them - they probably just want to vent</li>
<li>Put walls of text behind a subject (CW) - helps the timeline not get flooded with text</li>
</ul>
</li>
<li>
<h3>Other</h3>
<p>If you're here and you happen to play minecraft, feel free to message me with your username and come play with us sometime!</p>
</li>
</ol>
<p>So I guess yeah, that's about it. Try to be nice, eh? We're probably all sad here.</p>
<br>
<img src="/static/logo.png" style="display: block; margin: auto; max-width: 100%; height: 50px; object-fit: contain;" />

BIN
static/blurhash-overlay.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.8 KiB

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

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

View file

@ -1350,6 +1350,13 @@
minimatch "^3.0.4" minimatch "^3.0.4"
strip-json-comments "^3.1.1" strip-json-comments "^3.1.1"
"@floatingghost/pinch-zoom-element@^1.3.1":
version "1.3.1"
resolved "https://registry.yarnpkg.com/@floatingghost/pinch-zoom-element/-/pinch-zoom-element-1.3.1.tgz#5f327ad17ddf1f56777098aca088fdbf99cbd049"
integrity sha512-KnE7aBQdd/Fj1TzU5uzgwD9YAQ58DTMUks/PoTEBFW4zi0lBM9cN/j45wzcnzsT2VXG1S6qM7NMmq7NGm2//Fg==
dependencies:
pointer-tracker "^2.0.3"
"@fortawesome/fontawesome-common-types@6.2.0": "@fortawesome/fontawesome-common-types@6.2.0":
version "6.2.0" version "6.2.0"
resolved "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.2.0.tgz" resolved "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.2.0.tgz"
@ -1516,13 +1523,6 @@
"@jridgewell/resolve-uri" "^3.0.3" "@jridgewell/resolve-uri" "^3.0.3"
"@jridgewell/sourcemap-codec" "^1.4.10" "@jridgewell/sourcemap-codec" "^1.4.10"
"@kazvmoe-infra/pinch-zoom-element@1.2.0":
version "1.2.0"
resolved "https://registry.npmjs.org/@kazvmoe-infra/pinch-zoom-element/-/pinch-zoom-element-1.2.0.tgz"
integrity sha512-HBrhH5O/Fsp2bB7EGTXzCsBAVcMjknSagKC5pBdGpKsF8meHISR0kjDIdw4YoE0S+0oNMwJ6ZUZyIBrdywxPPw==
dependencies:
pointer-tracker "^2.0.3"
"@nicolo-ribaudo/eslint-scope-5-internals@5.1.1-v1": "@nicolo-ribaudo/eslint-scope-5-internals@5.1.1-v1":
version "5.1.1-v1" version "5.1.1-v1"
resolved "https://registry.yarnpkg.com/@nicolo-ribaudo/eslint-scope-5-internals/-/eslint-scope-5-internals-5.1.1-v1.tgz#dbf733a965ca47b1973177dc0bb6c889edcfb129" resolved "https://registry.yarnpkg.com/@nicolo-ribaudo/eslint-scope-5-internals/-/eslint-scope-5-internals-5.1.1-v1.tgz#dbf733a965ca47b1973177dc0bb6c889edcfb129"
@ -2494,6 +2494,11 @@ binary-extensions@^2.0.0:
resolved "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz" resolved "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz"
integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA== integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==
blurhash@^2.0.4:
version "2.0.4"
resolved "https://registry.yarnpkg.com/blurhash/-/blurhash-2.0.4.tgz#60642a823b50acaaf3732ddb6c7dfd721bdfef2a"
integrity sha512-r/As72u2FbucLoK5NTegM/GucxJc3d8GvHc4ngo13IO/nt2HU4gONxNLq1XPN6EM/V8Y9URIa7PcSz2RZu553A==
body-parser@1.19.2: body-parser@1.19.2:
version "1.19.2" version "1.19.2"
resolved "https://registry.npmjs.org/body-parser/-/body-parser-1.19.2.tgz" resolved "https://registry.npmjs.org/body-parser/-/body-parser-1.19.2.tgz"
@ -5090,9 +5095,9 @@ isexe@^2.0.0:
resolved "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz" resolved "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz"
integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw== integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==
iso-639-1@2.1.15: iso-639-1@^2.1.15:
version "2.1.15" version "2.1.15"
resolved "https://registry.npmjs.org/iso-639-1/-/iso-639-1-2.1.15.tgz" resolved "https://registry.yarnpkg.com/iso-639-1/-/iso-639-1-2.1.15.tgz#20cf78a4f691aeb802c16f17a6bad7d99271e85d"
integrity sha512-7c7mBznZu2ktfvyT582E2msM+Udc1EjOyhVRE/0ZsjD9LBtWSm23h3PtiRh2a35XoUsTQQjJXaJzuLjXsOdFDg== integrity sha512-7c7mBznZu2ktfvyT582E2msM+Udc1EjOyhVRE/0ZsjD9LBtWSm23h3PtiRh2a35XoUsTQQjJXaJzuLjXsOdFDg==
isobject@^3.0.1: isobject@^3.0.1: