Compare commits

...
Sign in to create a new pull request.

13 commits

Author SHA1 Message Date
48905a4431 Merge pull request 'a fix for nsfw warnings display on webkit' (#488) from mkljczk/akkoma-fe:webkit-fix into develop
Reviewed-on: AkkomaGang/akkoma-fe#488
2026-03-06 16:30:12 +00:00
c465cb0a35 components/attachment: fix display of nsfw overlays on webkit 2026-03-06 00:00:00 +00:00
Oneric
affbc240d1 changelog: add everything since 3.17 (2025.12) 2026-03-02 00:00:00 +00:00
a123b41a2f Merge pull request 'Fix HTML attribute parsing for escaped quotes' (#480) from mkljczk/akkoma-fe:get-attrs-fix into develop
Reviewed-on: AkkomaGang/akkoma-fe#480
Reviewed-by: Oneric <oneric@noreply.akkoma>
2026-02-19 12:31:59 +00:00
4ab3424508 Fix HTML attribute parsing for escaped quotes
Signed-off-by: nicole mikołajczyk <git@mkljczk.pl>
2026-02-16 13:36:15 +01:00
b04e4810f8 Fix HTML attribute parsing, discard attributes not strating with a letter
Signed-off-by: nicole mikołajczyk <git@mkljczk.pl>
2026-02-16 13:30:52 +01:00
fc8debd2c4 Merge pull request 'components/quote_button: show for local and own private posts' (#478) from Oneric/akkoma-fe:more-quoting into develop
Reviewed-on: AkkomaGang/akkoma-fe#478
2026-02-07 22:40:22 +00:00
Oneric
8227c84aa2 components/quote_button: show for local and own private posts
Aligning to AkkomaGang/akkoma#1059
2026-02-07 00:00:00 +00:00
Oneric
42595fcb2c cosmetic: fix linter complaints
Mostly just reordering, whitespace changes
and removing superfluous "this".

eslint really wants us to add :key to the UserAvatar list in DM
conversation cards. With :key Vue will reorder elements instead
of patching their contents on list changes, allowing input state
of elements to be preserved. This doesn’t really seem relevant
here since USerAvatars do not have a state, but also not harmful.

One lint complaint about using double quotes at the outer level
was purposefully ignored as it results in needing to quote
double quotes within the string making it rather unreadable.
2026-01-26 00:00:00 +00:00
Oneric
e3a72827ef side_drawer: add entry for bookmarks
It was not easily available in the narrow "mobile" interface
until now since both the desktop_nav and top nav panel are hidden.
Placing bookmarks after lists is consistent with the top nav panel
(though the top nav panel also puts interactions before both).

The recently removed "direct" timeline was similarly unavailable,
but its replacement, dm conversations, was already added to the
side drawer upon its introduction.

Fixes: AkkomaGang/akkoma-fe#474
2026-01-25 00:00:00 +00:00
34e4928754 Merge pull request 'polls: don't continuously refresh closed polls and refresh less frequently' (#472) from Oneric/akkoma-fe:poll-upd-frequency-reduction into develop
Reviewed-on: AkkomaGang/akkoma-fe#472
2026-01-24 18:29:59 +00:00
Oneric
9bfd3936d6 polls: do not fetch updates for closed polls 2026-01-14 00:00:00 +00:00
Oneric
8d8e6d979a polls: reduce frequency of update fetches
Thirty seconds is much quicker than any other auto-refreshes in the
interface. Emitting a request for every users and tab with the poll
loaded this frequently can add up to a noteworthy total on the backend.

This is significantly worsened by our backend currently synchronously
fetching and updating the status of remote polls when queried about
while initiating a remote fetch for every incoming request inbetween
the last refetch passing the  age threshold and the first current fetch
suceeding and completing its db transaction.
2026-01-14 00:00:00 +00:00
18 changed files with 141 additions and 75 deletions

View file

@ -3,12 +3,28 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
## Unreleased
## Unreleased (3.18)
### REMOVED
- dropped obsolete and buggy dm timeline
### Added
- UI for conversations API, replacing the DM timeline.
Here each thread (conversation) has its own timeline and read markers instead of mixing everything together.
- Boosts now show when and with which visibility they were boosted
- bookmarks are now accessible via the narrow/mobile UI
### Fixed
- fixed saving fallback cop yof settings to local browser storage
- improve image animation detection further
- fix status content parsing for mention and hashtag detection; this could lock the UI until reload
- fix display of nsfw attachment overlays on webkit
## Between 2022.09 (3.2.0) and 2025.12 (3.17.0)
A whole lot of stuff, but we forgot to update the changelog besides the one entry below, oopsi
- Implemented remote interaction with statuses
## 2022.09 - 2022-09-10
## 2022.09 (3.2.0) - 2022-09-10
### Added
- Automatic post translations. Must be configured on the backend in order to work.
- Post editing, including a log of previous edits.

View file

@ -26,6 +26,12 @@
}
}
&.-nsfw-placeholder {
.attachment-wrapper {
align-content: unset;
}
}
.description-container {
flex: 0 1 0;
display: flex;

View file

@ -6,8 +6,9 @@
<div class="heading">
<div class="title-bar">
<div class="title-bar-left">
<div class="unread"
<div
v-if="conversation.unread"
class="unread"
>
<span
class="badge badge-notification"
@ -29,7 +30,7 @@
</button>
&nbsp;
</div>
<h4>{{ $t('dm_conv.default_name', {id: this.conversation.id}) }}</h4>
<h4>{{ $t('dm_conv.default_name', {id: conversation.id}) }}</h4>
</div>
<div class="title-bar-right">
<button
@ -48,17 +49,21 @@
<div class="members">
<UserAvatar
v-for="user in membersTruncated.users"
:key="user.id"
:user="user"
:compact="compact"
/>
<div v-if="membersTruncated.truncated" class="ellipsis">
<div
v-if="membersTruncated.truncated"
class="ellipsis"
>
...
</div>
</div>
</router-link>
<div
class="last-message"
v-if="showLastStatus"
class="last-message"
>
<div class="last-message-title">
{{ $t('dm_conv.last_message_title') }}:
@ -76,7 +81,7 @@
<button
class="btn button-default"
:title="$t('dm_conv.recipients_edit_mode_button_tooltip')"
@click.once="$router.push({ name: 'dm-conversation-recipients', params: { id: this.conversation.id }})"
@click.once="$router.push({ name: 'dm-conversation-recipients', params: { id: conversation.id }})"
>
{{ $t('dm_conv.recipients_edit_mode_button') }}
</button>
@ -90,7 +95,7 @@
@accepted="doDeleteConversation"
@cancelled="hideDeleteConfirmModal"
>
{{ $t('dm_conv.delete_confirm', { identifier: this.conversation.id }) }}
{{ $t('dm_conv.delete_confirm', { identifier: conversation.id }) }}
</confirm-modal>
</teleport>
</div>

View file

@ -6,15 +6,15 @@
timeline-name="dmConv"
>
<template
v-slot:extraHeading
#extraHeading
>
<DMConvCard
v-if="conversation"
:conversation="conversation"
:compact="false"
:showFullControls="true"
:showLastStatus="false"
:linkToTimeline="false"
:show-full-controls="true"
:show-last-status="false"
:link-to-timeline="false"
@deleted="forceLeave"
/>
</template>

View file

@ -37,7 +37,7 @@
<StillImage
v-else
:src="item.emoji.imageUrl"
noStopGifs="true"
no-stop-gifs="true"
/>
</span>
</template>

View file

@ -51,7 +51,7 @@
<StillImage
v-if="suggestion.img"
:src="suggestion.img"
noStopGifs="true"
no-stop-gifs="true"
/>
<span v-else>{{ suggestion.replacement }}</span>
</span>

View file

@ -21,7 +21,7 @@
<StillImage
v-else
:src="group.first.imageUrl"
noStopGifs="true"
no-stop-gifs="true"
/>
</span>
<span

View file

@ -31,7 +31,7 @@
:alt="currentMedia.description"
:title="currentMedia.description"
:image-load-handler="onImageLoaded"
noStopGifs="true"
no-stop-gifs="true"
/>
</PinchZoom>
</SwipeClick>

View file

@ -195,8 +195,8 @@
:class="{ 'visibility-tray-edit': isEdit }"
>
<scope-selector
ref="scopeselector"
v-if="!disableVisibilitySelector"
ref="scopeselector"
:user-default="userDefaultScope"
:original-scope="copyMessageScope"
:initial-scope="newStatus.visibility"
@ -204,10 +204,11 @@
/>
<div
class="format-selector-container">
class="format-selector-container"
>
<div
class="format-selector"
>
>
<Select
id="post-language"
v-model="newStatus.language"

View file

@ -7,8 +7,16 @@ const QuoteButton = {
name: 'QuoteButton',
props: ['status', 'quoting', 'visibility'],
computed: {
loggedIn () {
return !!this.$store.state.users.currentUser
showButton () {
const currentUserId = this.$store.state.users.currentUser?.id
if (!currentUserId)
return false
if (['public', 'unlisted', 'local'].includes(this.visibility))
return true
return (this.visibility === 'private' && currentUserId == this.status.user.id)
}
}
}

View file

@ -1,6 +1,6 @@
<template>
<div
v-if="(visibility === 'public' || visibility === 'unlisted') && loggedIn"
v-if="showButton"
class="QuoteButton"
>
<button

View file

@ -9,6 +9,7 @@ import {
faHome,
faComments,
faBolt,
faBookmark,
faUserPlus,
faBullhorn,
faSearch,
@ -25,6 +26,7 @@ library.add(
faHome,
faComments,
faBolt,
faBookmark,
faUserPlus,
faBullhorn,
faSearch,

View file

@ -85,6 +85,18 @@
/> {{ $t("nav.lists") }}
</router-link>
</li>
<li
v-if="currentUser"
@click="toggleDrawer"
>
<router-link :to="{ name: 'bookmarks' }">
<FAIcon
fixed-width
class="fa-scale-110 fa-old-padding"
icon="bookmark"
/> {{ $t("nav.bookmarks") }}
</router-link>
</li>
</ul>
<ul v-if="currentUser">
<li @click="toggleDrawer">

View file

@ -99,19 +99,21 @@
<router-link
v-else
:to="retweeterProfileLink"
>{{ retweeter }}</router-link>
>
{{ retweeter }}
</router-link>
</div>
{{ ' ' }}
<div
class="repeat-tooltip"
>
<FAIcon
icon="retweet"
class="repeat-icon"
:title="$t('tool_tip.repeat')"
/>
{{ $t('timeline.repeated') }}
<FAIcon
icon="retweet"
class="repeat-icon"
:title="$t('tool_tip.repeat')"
/>
{{ $t('timeline.repeated') }}
</div>
<span

View file

@ -56,7 +56,7 @@
v-if="$slots.extraHeading"
class="timeline-extra-heading"
>
<slot name="extraHeading"></slot>
<slot name="extraHeading" />
</div>
<div :class="classes.body">
<div

View file

@ -11,7 +11,7 @@
<div class="panel-heading -flexible-height">
<div
class="user-info"
:class="{ '-compact': this.compactUserInfo }"
:class="{ '-compact': compactUserInfo }"
>
<div class="container">
<a
@ -54,7 +54,10 @@
>
@{{ user.screen_name_ui }}
</router-link>
<span class="user-roles" v-if="!hideBio && (user.deactivated || !!visibleRole || user.bot)">
<span
v-if="!hideBio && (user.deactivated || !!visibleRole || user.bot)"
class="user-roles"
>
<span
v-if="user.deactivated"
class="alert user-role"
@ -74,7 +77,10 @@
{{ $t('user_card.bot') }}
</span>
</span>
<span class="user-locked" v-if="user.locked">
<span
v-if="user.locked"
class="user-locked"
>
<FAIcon
class="lock-icon"
icon="lock"
@ -114,44 +120,44 @@
</div>
<div class="user-buttons">
<button
v-if="!isOtherUser && user.is_local"
class="button-unstyled edit-profile-button"
@click.stop="openProfileTab"
>
<FAIcon
fixed-width
class="icon"
icon="edit"
:title="$t('user_card.edit_profile')"
/>
</button>
<a
v-if="isOtherUser && !user.is_local"
:href="user.statusnet_profile_url"
target="_blank"
class="button-unstyled external-link-button"
>
<FAIcon
class="icon"
icon="external-link-alt"
/>
</a>
<a
v-if="isOtherUser"
:href="user.statusnet_profile_url + '.rss'"
target="_blank"
class="button-unstyled external-link-button"
>
<FAIcon
class="icon"
icon="rss"
/>
</a>
<AccountActions
v-if="isOtherUser && loggedIn"
:user="user"
:relationship="relationship"
v-if="!isOtherUser && user.is_local"
class="button-unstyled edit-profile-button"
@click.stop="openProfileTab"
>
<FAIcon
fixed-width
class="icon"
icon="edit"
:title="$t('user_card.edit_profile')"
/>
</button>
<a
v-if="isOtherUser && !user.is_local"
:href="user.statusnet_profile_url"
target="_blank"
class="button-unstyled external-link-button"
>
<FAIcon
class="icon"
icon="external-link-alt"
/>
</a>
<a
v-if="isOtherUser"
:href="user.statusnet_profile_url + '.rss'"
target="_blank"
class="button-unstyled external-link-button"
>
<FAIcon
class="icon"
icon="rss"
/>
</a>
<AccountActions
v-if="isOtherUser && loggedIn"
:user="user"
:relationship="relationship"
/>
</div>
</div>
<div class="user-meta">
@ -303,7 +309,7 @@
:html="user.description_html"
:emoji="user.emoji"
:handle-links="true"
:style='{"text-align": this.$store.getters.mergedConfig.centerAlignBio ? "center" : "start"}'
:style='{"text-align": $store.getters.mergedConfig.centerAlignBio ? "center" : "start"}'
/>
</div>
<teleport to="#modal">

View file

@ -1,5 +1,7 @@
import { merge } from 'lodash'
const POLL_UPDATE_FREQUENCY = 150_000;
const polls = {
state: {
// Contains key = id, value = number of trackers for this poll
@ -12,6 +14,9 @@ const polls = {
// Make expired-state change trigger re-renders properly
poll.expired = Date.now() > Date.parse(poll.expires_at)
if (existingPoll) {
if (poll.expired) {
state.trackedPolls[poll.id] = 0
}
state.pollsObject[poll.id] = merge(existingPoll, poll)
} else {
state.pollsObject[poll.id] = poll
@ -44,13 +49,16 @@ const polls = {
if (rootState.polls.trackedPolls[pollId]) {
dispatch('updateTrackedPoll', pollId)
}
}, 30 * 1000)
}, POLL_UPDATE_FREQUENCY)
commit('mergeOrAddPoll', poll)
})
},
trackPoll ({ rootState, commit, dispatch }, pollId) {
if (rootState.polls.pollsObject[pollId]?.expired)
return;
if (!rootState.polls.trackedPolls[pollId]) {
setTimeout(() => dispatch('updateTrackedPoll', pollId), 30 * 1000)
setTimeout(() => dispatch('updateTrackedPoll', pollId), POLL_UPDATE_FREQUENCY)
}
commit('trackPoll', pollId)
},

View file

@ -22,7 +22,7 @@ export const getAttrs = tag => {
.replace(new RegExp('^' + getTagName(tag)), '')
.replace(/\/?$/, '')
.trim()
const attrs = Array.from(innertag.matchAll(/([a-z0-9-]+)(?:=("[^"]+?"|'[^']+?'))?/gi))
const attrs = Array.from(innertag.matchAll(/([a-z]+[a-z0-9-]*)(?:=((?:"(?:\\.|[^"\\])*")|(?:'(?:\\.|[^'\\])*')))?/gi))
.map(([trash, key, value]) => [key, value])
.map(([k, v]) => {
if (!v) return [k, true]