add support for misskey-like cat fields (is cat & speak as cat) and frontend options for this

This commit is contained in:
notfire 2025-09-07 15:02:04 -04:00
commit 0b05ec0ce1
No known key found for this signature in database
15 changed files with 243 additions and 3 deletions

View file

@ -32,8 +32,15 @@
- emojis are sourced from discord's list of emojis - emojis are sourced from discord's list of emojis
- emojis are now sorted by category (like on every other emoji picker) instead of alphabetically - emojis are now sorted by category (like on every other emoji picker) instead of alphabetically
- script to get new emojis is included for future unicode versions - script to get new emojis is included for future unicode versions
- cat stuff!!
- toggles for:
- showing "cat" badge on user profiles
- showing cat ears behind users' avatars
- "nyaize"-ing posts from authors with "Speak as Cat" enabled
- this requires [a backend patch](https://git.notfire.cc/notfire/iceshrimp-patches/src/branch/main/0007-cat-fields-in-akko-api.patch)
- add toggle for showing post edit notifications - add toggle for showing post edit notifications
- add support for accepted follow request notifications - add support for accepted follow request notifications
- this requires [a backend patch](https://git.notfire.cc/notfire/iceshrimp-patches/src/branch/main/0005-previews-in-mastoapi.patch)
- add option to prevent page from getting pushed when making a new post with streaming api enabled (similar to 3rd "for unreads" option) - add option to prevent page from getting pushed when making a new post with streaming api enabled (similar to 3rd "for unreads" option)
- add link on user card to open profiles in iceshrimp-fe - add link on user card to open profiles in iceshrimp-fe
- add option to open conversation view by clicking empty space in posts - add option to open conversation view by clicking empty space in posts
@ -42,6 +49,11 @@
- add a script to download ruffle for flash support (requires python3 and requests library) - add a script to download ruffle for flash support (requires python3 and requests library)
- download by entering `tools/` and running `python3 download_ruffle.py` - download by entering `tools/` and running `python3 download_ruffle.py`
### also of note for anyone running this fork
- aside from the stuff mentioned above, the following features will require patches to work:
- mfm ([patch](https://git.notfire.cc/notfire/iceshrimp-patches/src/branch/main/0004-mfm-in-mastoapi.patch))
- previewing posts ([patch](https://git.notfire.cc/notfire/iceshrimp-patches/src/branch/main/0005-previews-in-mastoapi.patch))
--- ---
# Akkoma-FE # Akkoma-FE

View file

@ -32,6 +32,7 @@
"cropperjs": "^1.6.2", "cropperjs": "^1.6.2",
"diff": "^5.2.0", "diff": "^5.2.0",
"escape-html": "^1.0.3", "escape-html": "^1.0.3",
"fast-average-color": "^9.5.0",
"iso-639-1": "^2.1.15", "iso-639-1": "^2.1.15",
"js-cookie": "^3.0.1", "js-cookie": "^3.0.1",
"localforage": "^1.10.0", "localforage": "^1.10.0",

View file

@ -180,6 +180,7 @@ const setSettings = async ({ apiConfig, staticConfig, store }) => {
copyInstanceOption('alwaysShowSubjectInput') copyInstanceOption('alwaysShowSubjectInput')
copyInstanceOption('showFeaturesPanel') copyInstanceOption('showFeaturesPanel')
copyInstanceOption('hideSitename') copyInstanceOption('hideSitename')
copyInstanceOption('showCatFields')
copyInstanceOption('renderMisskeyMarkdown') copyInstanceOption('renderMisskeyMarkdown')
copyInstanceOption('sidebarRight') copyInstanceOption('sidebarRight')

View file

@ -135,6 +135,9 @@ const GeneralTab = {
this.$store.dispatch('setOption', { name: 'postLanguage', value: val }) this.$store.dispatch('setOption', { name: 'postLanguage', value: val })
} }
}, },
catOptsAvail: {
get: function () { return this.$store.getters.mergedConfig.showCatFields }
},
...SharedComputedObject() ...SharedComputedObject()
}, },
methods: { methods: {

View file

@ -319,6 +319,35 @@
{{ $t('settings.search_pagination_limit') }} {{ $t('settings.search_pagination_limit') }}
</IntegerSetting> </IntegerSetting>
</li> </li>
<li
v-if="catOptsAvail"
>
<h3>{{ $t('settings.cat_settings') }}</h3>
</li>
<li>
<BooleanSetting
path="showIsCatOnUserCard"
v-if="catOptsAvail"
>
{{ $t('settings.show_is_cat_on_user_card') }}
</BooleanSetting>
</li>
<li>
<BooleanSetting
path="showCatEars"
v-if="catOptsAvail"
>
{{ $t('settings.show_cat_ears') }}
</BooleanSetting>
</li>
<li>
<BooleanSetting
path="nyaizePosts"
v-if="catOptsAvail"
>
{{ $t('settings.nyaize_posts') }}
</BooleanSetting>
</li>
<li> <li>
<h3>{{ $t('settings.columns') }}</h3> <h3>{{ $t('settings.columns') }}</h3>
</li> </li>

View file

@ -86,6 +86,9 @@ const StatusContent = {
translationLanguages () { translationLanguages () {
return (this.$store.state.instance.supportedTranslationLanguages.source || []).map(lang => ({ key: lang.code, value: lang.code, label: lang.name })) return (this.$store.state.instance.supportedTranslationLanguages.source || []).map(lang => ({ key: lang.code, value: lang.code, label: lang.name }))
}, },
canSpeakAsCat () {
return (this.$store.getters.mergedConfig.nyaizePosts && this.status.user.speak_as_cat)
},
...mapGetters(['mergedConfig']) ...mapGetters(['mergedConfig'])
}, },
components: { components: {
@ -140,6 +143,16 @@ const StatusContent = {
this.$store.dispatch( this.$store.dispatch(
'translateStatus', { id: this.status.id, language: translateTo, from: this.translateFrom } 'translateStatus', { id: this.status.id, language: translateTo, from: this.translateFrom }
).finally(() => { this.translating = false }) ).finally(() => { this.translating = false })
},
speakAsCat () {
// taken from https://github.com/misskey-dev/misskey/blob/develop/packages/misskey-js/src/nyaize.ts
const enRegex1 = /(?<=n)a/gi
const enRegex2 = /(?<=morn)ing/gi
const enRegex3 = /(?<=every)one/gi
return this.status.text
.replace(enRegex1, x => x === 'A' ? 'YA' : 'ya')
.replace(enRegex2, x => x === 'ING' ? 'YAN' : 'yan')
.replace(enRegex3, x => x === 'ONE' ? 'NYAN' : 'nyan')
} }
} }
} }

View file

@ -48,7 +48,7 @@
<RichContent <RichContent
:class="{ '-single-line': singleLine }" :class="{ '-single-line': singleLine }"
class="text media-body" class="text media-body"
:html="status.raw_html" :html="canSpeakAsCat ? speakAsCat() : status.raw_html"
:emoji="status.emojis" :emoji="status.emojis"
:handle-links="true" :handle-links="true"
:mfm="renderMisskeyMarkdown && (status.media_type === 'text/x.misskeymarkdown')" :mfm="renderMisskeyMarkdown && (status.media_type === 'text/x.misskeymarkdown')"

View file

@ -1,6 +1,7 @@
import StillImage from '../still-image/still-image.vue' import StillImage from '../still-image/still-image.vue'
import { library } from '@fortawesome/fontawesome-svg-core' import { library } from '@fortawesome/fontawesome-svg-core'
import { FastAverageColor } from "fast-average-color";
import { import {
faRobot faRobot
@ -20,7 +21,9 @@ const UserAvatar = {
data () { data () {
return { return {
showPlaceholder: false, showPlaceholder: false,
defaultAvatar: `${this.$store.state.instance.server + this.$store.state.instance.defaultAvatar}` defaultAvatar: `${this.$store.state.instance.server + this.$store.state.instance.defaultAvatar}`,
earsReady: false,
earsColor: "white"
} }
}, },
components: { components: {
@ -33,6 +36,26 @@ const UserAvatar = {
imageLoadError () { imageLoadError () {
this.showPlaceholder = true this.showPlaceholder = true
} }
},
computed: {
canShowEars () {
return this.$store.getters.mergedConfig.showCatEars
}
},
mounted () {
if (this.canShowEars) {
const fac = new FastAverageColor()
for (let child of this.$el.children) {
let img = child.querySelector("img")
if (img) {
img.crossOrigin = "anonymous";
img.onload = () => {
this.earsColor = fac.getColor(img).hex;
this.earsReady = true;
}
}
}
}
} }
} }

View file

@ -3,6 +3,14 @@
class="Avatar" class="Avatar"
:class="{ '-compact': compact }" :class="{ '-compact': compact }"
> >
<div
v-if="user && user.is_cat && canShowEars && !showPlaceholder && earsReady"
class="ears"
:style="{'color': earsColor}"
>
<div class="earLeft" />
<div class="earRight" />
</div>
<StillImage <StillImage
v-if="user" v-if="user"
class="avatar" class="avatar"
@ -17,6 +25,7 @@
class="avatar -placeholder" class="avatar -placeholder"
:class="{ '-compact': compact }" :class="{ '-compact': compact }"
/> />
<FAIcon <FAIcon
v-if="bot" v-if="bot"
icon="robot" icon="robot"
@ -29,6 +38,7 @@
<style lang="scss"> <style lang="scss">
@import '../../_variables.scss'; @import '../../_variables.scss';
/* ears ripped from https://github.com/ihateblueb/aster/blob/main/packages/frontend/src/lib/components/Avatar.svelte */
.Avatar { .Avatar {
--_avatarShadowBox: var(--avatarStatusShadow); --_avatarShadowBox: var(--avatarStatusShadow);
--_avatarShadowFilter: var(--avatarStatusShadowFilter); --_avatarShadowFilter: var(--avatarStatusShadowFilter);
@ -40,6 +50,17 @@
width: 48px; width: 48px;
height: 48px; height: 48px;
&:hover {
.ears {
.earLeft {
animation: earwiggleleft 1s infinite;
}
.earRight {
animation: earwiggleright 1s infinite;
}
}
}
&.-compact { &.-compact {
width: 32px; width: 32px;
height: 32px; height: 32px;
@ -90,5 +111,116 @@
border-radius: var(--tooltipRadius); border-radius: var(--tooltipRadius);
} }
.ears {
contain: strict;
position: absolute;
z-index: 0;
display: flex;
top: -50%;
left: -50%;
width: 100%;
height: 100%;
padding: 50%;
pointer-events: none;
.earLeft,
.earRight {
contain: strict;
display: inline-block;
height: 50%;
width: 50%;
background: currentColor;
&::after {
contain: strict;
content: '';
display: block;
width: 60%;
height: 60%;
margin: 20%;
background: #df548f;
}
}
.earLeft {
transform: rotate(37.5deg) skew(30deg);
&,
&::after {
border-radius: 25% 75% 75%;
}
}
.earRight {
transform: rotate(-37.5deg) skew(-30deg);
&,
&::after {
border-radius: 75% 25% 75% 75%;
}
}
}
@keyframes earwiggleleft {
from {
transform: rotate(37.6deg) skew(30deg);
}
25% {
transform: rotate(10deg) skew(30deg);
}
50% {
transform: rotate(20deg) skew(30deg);
}
75% {
transform: rotate(0deg) skew(30deg);
}
to {
transform: rotate(37.6deg) skew(30deg);
}
}
@keyframes earwiggleright {
from {
transform: rotate(-37.6deg) skew(-30deg);
}
30% {
transform: rotate(-10deg) skew(-30deg);
}
55% {
transform: rotate(-20deg) skew(-30deg);
}
75% {
transform: rotate(0deg) skew(-30deg);
}
to {
transform: rotate(-37.6deg) skew(-30deg);
}
}
@keyframes eartightleft {
from {
transform: rotate(37.6deg) skew(30deg);
}
50% {
transform: rotate(37.4deg) skew(30deg);
}
to {
transform: rotate(37.6deg) skew(30deg);
}
}
@keyframes eartightright {
from {
transform: rotate(-37.6deg) skew(-30deg);
}
50% {
transform: rotate(-37.4deg) skew(-30deg);
}
to {
transform: rotate(-37.6deg) skew(-30deg);
}
}
} }
</style> </style>

View file

@ -54,7 +54,7 @@
> >
@{{ user.screen_name_ui }} @{{ user.screen_name_ui }}
</router-link> </router-link>
<span class="user-roles" v-if="!hideBio && (user.deactivated || !!visibleRole || user.bot)"> <span class="user-roles" v-if="(!hideBio && (user.deactivated || !!visibleRole || user.bot)) || (user.is_cat)">
<span <span
v-if="user.deactivated" v-if="user.deactivated"
class="alert user-role" class="alert user-role"
@ -73,6 +73,12 @@
> >
{{ $t('user_card.bot') }} {{ $t('user_card.bot') }}
</span> </span>
<span
v-if="mergedConfig.showIsCatOnUserCard && user.is_cat"
class="alert user-role"
>
{{ $t('user_card.is_cat') }}
</span>
</span> </span>
<span class="user-locked" v-if="user.locked"> <span class="user-locked" v-if="user.locked">
<FAIcon <FAIcon

View file

@ -502,6 +502,7 @@
"boosts_follow_def_vis": "Make boosts follow default visibility", "boosts_follow_def_vis": "Make boosts follow default visibility",
"bot": "This is a bot account", "bot": "This is a bot account",
"btnRadius": "Buttons", "btnRadius": "Buttons",
"cat_settings": "Cat settings",
"center_align_bio": "Center text in user bio", "center_align_bio": "Center text in user bio",
"cBlue": "Blue (Reply, follow)", "cBlue": "Blue (Reply, follow)",
"cGreen": "Green (Retweet)", "cGreen": "Green (Retweet)",
@ -708,6 +709,7 @@
"notification_visibility_repeats": "Boosts", "notification_visibility_repeats": "Boosts",
"notifications": "Notifications", "notifications": "Notifications",
"nsfw_clickthrough": "Hide sensitive/NSFW media", "nsfw_clickthrough": "Hide sensitive/NSFW media",
"nyaize_posts": "\"nyaize\" posts from authors with \"Speak as Cat\" enabled",
"oauth_tokens": "OAuth tokens", "oauth_tokens": "OAuth tokens",
"pad_emoji": "Pad emoji with spaces when adding from picker", "pad_emoji": "Pad emoji with spaces when adding from picker",
"panelRadius": "Panels", "panelRadius": "Panels",
@ -786,7 +788,9 @@
"settings_profiles_show": "Show all settings profiles", "settings_profiles_show": "Show all settings profiles",
"settings_profiles_unshow": "Hide all settings profiles", "settings_profiles_unshow": "Hide all settings profiles",
"show_admin_badge": "Show \"Admin\" badge in my profile", "show_admin_badge": "Show \"Admin\" badge in my profile",
"show_cat_ears": "Show cat ears behind users' avatars",
"show_favicon_badge": "Show a badge on the page's favicon when there are unread notifications", "show_favicon_badge": "Show a badge on the page's favicon when there are unread notifications",
"show_is_cat_on_user_card": "Show \"Cat\" badge on user profiles",
"show_moderator_badge": "Show \"Moderator\" badge in my profile", "show_moderator_badge": "Show \"Moderator\" badge in my profile",
"show_nav_shortcuts": "Show extra navigation shortcuts in top panel", "show_nav_shortcuts": "Show extra navigation shortcuts in top panel",
"show_panel_nav_shortcuts": "Show timeline navigation shortcuts at the top of the panel", "show_panel_nav_shortcuts": "Show timeline navigation shortcuts at the top of the panel",
@ -1212,6 +1216,7 @@
"striped": "Striped bg" "striped": "Striped bg"
}, },
"its_you": "It's you!", "its_you": "It's you!",
"is_cat": "Cat",
"media": "Media", "media": "Media",
"mention": "Mention", "mention": "Mention",
"message": "Message", "message": "Message",

View file

@ -134,6 +134,10 @@ export const defaultState = {
autoRefreshOnRequired: true, autoRefreshOnRequired: true,
showUnreadInTitle: true, showUnreadInTitle: true,
clickEmptyToOpenConversation: false, clickEmptyToOpenConversation: false,
showCatFields: undefined,
showIsCatOnUserCard: false,
showCatEars: false,
nyaizePosts: false,
renderMisskeyMarkdown: undefined, renderMisskeyMarkdown: undefined,
renderMfmOnHover: undefined, // instance default renderMfmOnHover: undefined, // instance default
conversationDisplay: undefined, // instance default conversationDisplay: undefined, // instance default

View file

@ -90,6 +90,10 @@ const defaultState = {
autoRefreshOnRequired: true, autoRefreshOnRequired: true,
showUnreadInTitle: true, showUnreadInTitle: true,
clickEmptyToOpenConversation: false, clickEmptyToOpenConversation: false,
showCatFields: false,
showIsCatOnUserCard: false,
showCatEars: false,
nyaizePosts: false,
renderMisskeyMarkdown: true, renderMisskeyMarkdown: true,
renderMfmOnHover: false, renderMfmOnHover: false,
conversationDisplay: 'linear', conversationDisplay: 'linear',

View file

@ -90,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.is_cat = typeof data.is_cat !== 'undefined' ? data.is_cat : false
output.speak_as_cat = typeof data.speak_as_cat !== 'undefined' ? data.speak_as_cat : false
output.accepts_direct_messages_from = data.accepts_direct_messages_from output.accepts_direct_messages_from = data.accepts_direct_messages_from
output.follow_requests_count = data.follow_requests_count output.follow_requests_count = data.follow_requests_count
if (data.akkoma) { if (data.akkoma) {

View file

@ -3998,6 +3998,11 @@ extract-zip@^2.0.1:
optionalDependencies: optionalDependencies:
"@types/yauzl" "^2.9.1" "@types/yauzl" "^2.9.1"
fast-average-color@^9.5.0:
version "9.5.0"
resolved "https://registry.yarnpkg.com/fast-average-color/-/fast-average-color-9.5.0.tgz"
integrity sha512-nC6x2YIlJ9xxgkMFMd1BNoM1ctMjNoRKfRliPmiEWW3S6rLTHiQcy9g3pt/xiKv/D0NAAkhb9VyV+WJFvTqMGg==
fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3:
version "3.1.3" version "3.1.3"
resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525"