Compare commits

...

6 commits

9 changed files with 166 additions and 8 deletions

View file

@ -1,3 +1,18 @@
A soft fork of Akkoma-FE fixing a few minor but longstanding annoyances.
Hopefully most of these will be merged upstream when I learn how to do PRs.
## Comprehensive List of Fixes and Changes
### Added
- Approve and deny follow request buttons to the user profile page
### Fixed
- Settings backup when the word filters contain non-ASCII characters
- The hover color of the checkmark icon on follow request notifications
# 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)

View file

@ -194,11 +194,11 @@ const ExtraButtons = {
}
},
showFediLinks () {
return this.$store.getters.mergedConfig.showFediLinks === true && this.status.visibility != 'direct'
return this.$store.getters.mergedConfig.showFediLinks === true
},
fediLinkURL () {
try {
return this.statusLink.replace(/^https?/, 'web+ap')
return this.status.external_uri.replace(/^https?/, 'web+ap')
} catch (e) {
return null
}

View file

@ -65,8 +65,8 @@
.follow-request-accept {
&:hover {
color: $fallback--text;
color: var(--text, $fallback--text);
color: $fallback--cGreen;
color: var(--cGreen, $fallback--text);
}
}

View file

@ -9,6 +9,7 @@ import RichContent from 'src/components/rich_content/rich_content.jsx'
import ConfirmModal from '../confirm_modal/confirm_modal.vue'
import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
import facActivityPub from '../../assets/icons/activity-pub'
import { notificationsFromStore } from 'src/services/notification_utils/notification_utils.js'
import { mapGetters } from 'vuex'
import { library } from '@fortawesome/fontawesome-svg-core'
import {
@ -36,7 +37,9 @@ export default {
return {
followRequestInProgress: false,
betterShadow: this.$store.state.interface.browserSupport.cssFilter,
showingConfirmMute: false
showingConfirmMute: false,
showingApproveConfirmDialog: false,
showingDenyConfirmDialog: false
}
},
created () {
@ -129,6 +132,12 @@ export default {
shouldConfirmMute () {
return this.mergedConfig.modalOnMute
},
shouldConfirmApprove () {
return this.mergedConfig.modalOnApproveFollow
},
shouldConfirmDeny () {
return this.mergedConfig.modalOnDenyFollow
},
...mapGetters(['mergedConfig'])
},
components: {
@ -205,6 +214,58 @@ export default {
},
mentionUser () {
this.$store.dispatch('openPostStatusModal', { replyTo: true, repliedUser: this.user })
},
showApproveConfirmDialog () {
this.showingApproveConfirmDialog = true
},
hideApproveConfirmDialog () {
this.showingApproveConfirmDialog = false
},
showDenyConfirmDialog () {
this.showingDenyConfirmDialog = true
},
hideDenyConfirmDialog () {
this.showingDenyConfirmDialog = false
},
approveUser () {
if (this.shouldConfirmApprove) {
this.showApproveConfirmDialog()
} else {
this.doApprove()
}
},
doApprove () {
const notifId = this.findFollowRequestNotificationId()
this.$store.dispatch('approveUser', this.user.id)
this.$store.dispatch('markSingleNotificationAsSeen', { id: notifId })
this.$store.dispatch('updateNotification', {
id: notifId,
updater: notification => {
notification.type = 'follow'
}
})
this.hideApproveConfirmDialog()
},
denyUser () {
if (this.shouldConfirmDeny) {
this.showDenyConfirmDialog()
} else {
this.doDeny()
}
},
doDeny () {
const notifId = this.findFollowRequestNotificationId()
this.$store.dispatch('denyUser', this.user.id)
.then(() => {
this.$store.dispatch('dismissNotificationLocal', { id: notifId })
})
this.hideDenyConfirmDialog()
},
findFollowRequestNotificationId () {
const notif = notificationsFromStore(this.$store).find(
(notif) => notif.from_profile.id === this.user.id && notif.type === 'follow_request'
)
return notif && notif.id
}
}
}

View file

@ -254,6 +254,12 @@
text-align: left;
}
.requested_by {
.btn {
margin-right: .25em;
}
}
.highlighter {
flex: 0 1 auto;
display: flex;

View file

@ -68,8 +68,32 @@
<div v-if="relationship.followed_by && loggedIn && isOtherUser" class="following">
{{ $t('user_card.follows_you') }}
</div>
<div v-if="relationship.requested_by && loggedIn && isOtherUser" class="requested_by">
<div
v-if="relationship.requested_by && loggedIn && isOtherUser"
class="requested_by"
style="white-space: nowrap;"
>
{{ $t('user_card.requested_by') }}
<button
class="btn button-default"
:title="$t('tool_tip.accept_follow_request')"
@click="approveUser()"
>
<FAIcon
icon="check"
class="fa-scale-110 fa-old-padding follow-request-accept"
/>
</button>
<button
class="btn button-default"
:title="$t('tool_tip.reject_follow_request')"
@click="denyUser()"
>
<FAIcon
icon="times"
class="fa-scale-110 fa-old-padding follow-request-reject"
/>
</button>
</div>
<div v-if="isOtherUser && (loggedIn || !switcher)" class="highlighter">
<!-- id's need to be unique, otherwise vue confuses which user-card checkbox belongs to -->
@ -161,6 +185,26 @@
</template>
</i18n-t>
</confirm-modal>
<confirm-modal
v-if="showingApproveConfirmDialog"
:title="$t('user_card.approve_confirm_title')"
:confirm-text="$t('user_card.approve_confirm_accept_button')"
:cancel-text="$t('user_card.approve_confirm_cancel_button')"
@accepted="doApprove"
@cancelled="hideApproveConfirmDialog"
>
{{ $t('user_card.approve_confirm', { user: user.screen_name_ui }) }}
</confirm-modal>
<confirm-modal
v-if="showingDenyConfirmDialog"
:title="$t('user_card.deny_confirm_title')"
:confirm-text="$t('user_card.deny_confirm_accept_button')"
:cancel-text="$t('user_card.deny_confirm_cancel_button')"
@accepted="doDeny"
@cancelled="hideDenyConfirmDialog"
>
{{ $t('user_card.deny_confirm', { user: user.screen_name_ui }) }}
</confirm-modal>
</teleport>
</div>
</template>

View file

@ -54,6 +54,26 @@ const unblockUser = (store, id) => {
.then((relationship) => store.commit('updateUserRelationship', [relationship]))
}
const approveUser = (store, id) => {
const predictedRelationship = store.state.relationships[id] || { id }
predictedRelationship.requested_by = false
predictedRelationship.followed_by = true
store.commit('updateUserRelationship', [predictedRelationship])
return store.rootState.api.backendInteractor.approveUser({ id })
.then((relationship) => store.commit('updateUserRelationship', [relationship]))
}
const denyUser = (store, id) => {
const predictedRelationship = store.state.relationships[id] || { id }
predictedRelationship.requested_by = false
predictedRelationship.followed_by = false
store.commit('updateUserRelationship', [predictedRelationship])
return store.rootState.api.backendInteractor.denyUser({ id })
.then((relationship) => store.commit('updateUserRelationship', [relationship]))
}
const removeUserFromFollowers = (store, id) => {
return store.rootState.api.backendInteractor.removeUserFromFollowers({ id })
.then((relationship) => store.commit('updateUserRelationship', [relationship]))
@ -344,6 +364,12 @@ const users = {
unblockUser (store, id) {
return unblockUser(store, id)
},
approveUser (store, id) {
return approveUser(store, id)
},
denyUser (store, id) {
return denyUser(store, id)
},
removeUserFromFollowers (store, id) {
return removeUserFromFollowers(store, id)
},

View file

@ -316,6 +316,7 @@ export const parseStatus = (data) => {
output.summary_raw_html = escape(data.spoiler_text)
output.external_url = data.url
output.external_uri = data.uri
output.poll = data.poll
if (output.poll) {
output.poll.options = (output.poll.options || []).map(field => ({

View file

@ -4,11 +4,14 @@ export const newExporter = ({
}) => ({
exportData () {
const stringified = JSON.stringify(getExportedObject(), null, 2) // Pretty-print and indent with 2 spaces
const bytes = new TextEncoder().encode(stringified)
const ascii = Array.from(bytes, (x) => String.fromCodePoint(x)).join("")
const data = window.btoa(ascii)
// Create an invisible link with a data url and simulate a click
const e = document.createElement('a')
e.setAttribute('download', `${filename}.json`)
e.setAttribute('href', 'data:application/json;base64,' + window.btoa(stringified))
e.setAttribute('href', 'data:application/json;base64,' + data)
e.style.display = 'none'
document.body.appendChild(e)
@ -33,7 +36,9 @@ export const newImporter = ({
const reader = new FileReader()
reader.onload = ({ target }) => {
try {
const parsed = JSON.parse(target.result)
const bytes = Uint8Array.from(target.result, (x) => x.codePointAt(0))
const data = new TextDecoder().decode(bytes)
const parsed = JSON.parse(data)
const validationResult = validator(parsed)
if (validationResult === true) {
onImport(parsed)