Compare commits

..

No commits in common. "61fc11a65ef1cf07f085481e003b1f761151da2f" and "3c32b48e29a29d853f5d1728745774fc0db80d05" have entirely different histories.

15 changed files with 77 additions and 199 deletions

View file

@ -1,33 +0,0 @@
kind: pipeline
name: default
steps:
- name: docker
image: plugins/docker
settings:
repo: docker.data.coop/pinafore
registry: docker.data.coop
username:
from_secret: DOCKER_USERNAME
password:
from_secret: DOCKER_PASSWORD
tags:
- "${DRONE_BUILD_NUMBER}"
- "${DRONE_TAG}"
- "latest"
when:
ref:
- refs/tags/*
event:
exclude:
- pull_request
- name: notify
image: plugins/matrix
settings:
homeserver: https://data.coop
roomid: plKSghHbepWeUEtbHE:data.coop
username:
from_secret: matrix_username
password:
from_secret: matrix_password

View file

@ -1,6 +1,5 @@
export default [
{ id: 'pinafore-logo', src: 'src/static/sailboat.svg', inline: true },
{ id: 'fa-arrow-left', src: 'src/thirdparty/font-awesome-svg-png/white/svg/arrow-left.svg' },
{ id: 'fa-bell', src: 'src/thirdparty/font-awesome-svg-png/white/svg/bell.svg', inline: true },
{ id: 'fa-bell-o', src: 'src/thirdparty/font-awesome-svg-png/white/svg/bell-o.svg' },
{ id: 'fa-users', src: 'src/thirdparty/font-awesome-svg-png/white/svg/users.svg', inline: true },

View file

@ -1,7 +1,7 @@
{
"name": "pinafore",
"description": "Alternative web client for Mastodon",
"version": "2.5.0",
"version": "2.4.0",
"type": "module",
"engines": {
"node": "^12.20.0 || ^14.13.1 || ^16.0.0 || ^18.0.0"

View file

@ -497,8 +497,6 @@ export default {
}: {description}`,
accountFollowedYou: '{name} followed you, {account}',
accountSignedUp: '{name} signed up, {account}',
accountRequestedFollow: '{name} requested to follow you, {account}',
accountReported: '{name} filed a report, {account}',
reblogCountsHidden: 'Boost counts hidden',
favoriteCountsHidden: 'Favorite counts hidden',
rebloggedTimes: `Boosted {count, plural,
@ -514,8 +512,6 @@ export default {
favoritedYou: 'favorited your toot',
followedYou: 'followed you',
edited: 'edited their toot',
requestedFollow: 'requested to follow you',
reported: 'filed a report',
signedUp: 'signed up',
posted: 'posted',
pollYouCreatedEnded: 'A poll you created has ended',

View file

@ -1,6 +1,9 @@
import { database } from '../_database/database.js'
import { decode as decodeBlurhash, init as initBlurhash } from '../_utils/blurhash.js'
import { mark, stop } from '../_utils/marks.js'
import { prepareToRehydrate, rehydrateStatusOrNotification } from './rehydrateStatusOrNotification.js'
import { get } from '../_utils/lodash-lite.js'
import { statusHtmlToPlainText } from '../_utils/statusHtmlToPlainText.js'
import { scheduleIdleTask } from '../_utils/scheduleIdleTask.js'
async function getNotification (instanceName, timelineType, timelineValue, itemId) {
return {
@ -18,10 +21,62 @@ async function getStatus (instanceName, timelineType, timelineValue, itemId) {
}
}
function tryInitBlurhash () {
try {
initBlurhash()
} catch (err) {
console.error('could not start blurhash worker', err)
}
}
function getActualStatus (statusOrNotification) {
return get(statusOrNotification, ['status']) ||
get(statusOrNotification, ['notification', 'status'])
}
async function decodeAllBlurhashes (statusOrNotification) {
const status = getActualStatus(statusOrNotification)
if (!status) {
return
}
const mediaWithBlurhashes = get(status, ['media_attachments'], [])
.concat(get(status, ['reblog', 'media_attachments'], []))
.filter(_ => _.blurhash)
if (mediaWithBlurhashes.length) {
mark(`decodeBlurhash-${status.id}`)
await Promise.all(mediaWithBlurhashes.map(async media => {
try {
media.decodedBlurhash = await decodeBlurhash(media.blurhash)
} catch (err) {
console.warn('Could not decode blurhash, ignoring', err)
}
}))
stop(`decodeBlurhash-${status.id}`)
}
}
async function calculatePlainTextContent (statusOrNotification) {
const status = getActualStatus(statusOrNotification)
if (!status) {
return
}
const originalStatus = status.reblog ? status.reblog : status
const content = originalStatus.content || ''
const mentions = originalStatus.mentions || []
// Calculating the plaintext from the HTML is a non-trivial operation, so we might
// as well do it in advance, while blurhash is being decoded on the worker thread.
await new Promise(resolve => {
scheduleIdleTask(() => {
originalStatus.plainTextContent = statusHtmlToPlainText(content, mentions)
resolve()
})
})
}
export function createMakeProps (instanceName, timelineType, timelineValue) {
let promiseChain = Promise.resolve()
prepareToRehydrate() // start blurhash early to save time
tryInitBlurhash() // start the blurhash worker a bit early to save time
async function fetchFromIndexedDB (itemId) {
mark(`fetchFromIndexedDB-${itemId}`)
@ -37,7 +92,10 @@ export function createMakeProps (instanceName, timelineType, timelineValue) {
async function getStatusOrNotification (itemId) {
const statusOrNotification = await fetchFromIndexedDB(itemId)
await rehydrateStatusOrNotification(statusOrNotification)
await Promise.all([
decodeAllBlurhashes(statusOrNotification),
calculatePlainTextContent(statusOrNotification)
])
return statusOrNotification
}

View file

@ -4,28 +4,18 @@ import { database } from '../_database/database.js'
import {
getPinnedStatuses
} from '../_api/pinnedStatuses.js'
import { prepareToRehydrate, rehydrateStatusOrNotification } from './rehydrateStatusOrNotification.js'
// Pinned statuses aren't a "normal" timeline, so their blurhashes/plaintext need to be calculated specially
async function rehydratePinnedStatuses (statuses) {
await Promise.all(statuses.map(status => rehydrateStatusOrNotification({ status })))
return statuses
}
export async function updatePinnedStatusesForAccount (accountId) {
const { currentInstance, accessToken } = store.get()
await cacheFirstUpdateAfter(
() => getPinnedStatuses(currentInstance, accessToken, accountId),
async () => {
return rehydratePinnedStatuses(await getPinnedStatuses(currentInstance, accessToken, accountId))
},
async () => {
prepareToRehydrate() // start blurhash early to save time
const pinnedStatuses = await database.getPinnedStatuses(currentInstance, accountId)
if (!pinnedStatuses || !pinnedStatuses.every(Boolean)) {
throw new Error('missing pinned statuses in idb')
}
return rehydratePinnedStatuses(pinnedStatuses)
return pinnedStatuses
},
statuses => database.insertPinnedStatuses(currentInstance, accountId, statuses),
statuses => {

View file

@ -1,67 +0,0 @@
import { get } from '../_utils/lodash-lite.js'
import { mark, stop } from '../_utils/marks.js'
import { decode as decodeBlurhash, init as initBlurhash } from '../_utils/blurhash.js'
import { scheduleIdleTask } from '../_utils/scheduleIdleTask.js'
import { statusHtmlToPlainText } from '../_utils/statusHtmlToPlainText.js'
function getActualStatus (statusOrNotification) {
return get(statusOrNotification, ['status']) ||
get(statusOrNotification, ['notification', 'status'])
}
export function prepareToRehydrate () {
// start the blurhash worker a bit early to save time
try {
initBlurhash()
} catch (err) {
console.error('could not start blurhash worker', err)
}
}
async function decodeAllBlurhashes (statusOrNotification) {
const status = getActualStatus(statusOrNotification)
if (!status) {
return
}
const mediaWithBlurhashes = get(status, ['media_attachments'], [])
.concat(get(status, ['reblog', 'media_attachments'], []))
.filter(_ => _.blurhash)
if (mediaWithBlurhashes.length) {
mark(`decodeBlurhash-${status.id}`)
await Promise.all(mediaWithBlurhashes.map(async media => {
try {
media.decodedBlurhash = await decodeBlurhash(media.blurhash)
} catch (err) {
console.warn('Could not decode blurhash, ignoring', err)
}
}))
stop(`decodeBlurhash-${status.id}`)
}
}
async function calculatePlainTextContent (statusOrNotification) {
const status = getActualStatus(statusOrNotification)
if (!status) {
return
}
const originalStatus = status.reblog ? status.reblog : status
const content = originalStatus.content || ''
const mentions = originalStatus.mentions || []
// Calculating the plaintext from the HTML is a non-trivial operation, so we might
// as well do it in advance, while blurhash is being decoded on the worker thread.
await new Promise(resolve => {
scheduleIdleTask(() => {
originalStatus.plainTextContent = statusHtmlToPlainText(content, mentions)
resolve()
})
})
}
// Do stuff that we need to do when the status or notification is fetched from the database,
// like calculating the blurhash or calculating the plain text content
export async function rehydrateStatusOrNotification (statusOrNotification) {
await Promise.all([
decodeAllBlurhashes(statusOrNotification),
calculatePlainTextContent(statusOrNotification)
])
}

View file

@ -66,7 +66,7 @@ export async function getTimeline (instanceName, accessToken, timeline, maxId, s
}
if (timeline === 'notifications/mentions') {
params.exclude_types = ['follow', 'favourite', 'reblog', 'poll', 'admin.sign_up', 'update', 'follow_request', 'admin.report']
params.exclude_types = ['follow', 'favourite', 'reblog', 'poll', 'admin.sign_up']
}
url += '?' + paramsString(params)

View file

@ -8,10 +8,7 @@
<button type="button"
class="dynamic-page-go-back"
aria-label="{intl.goBack}"
on:click|preventDefault="onGoBack()">
<SvgIcon className="dynamic-page-go-back-icon" href="#fa-arrow-left" />
{intl.back}
</button>
on:click|preventDefault="onGoBack()">{intl.back}</button>
</div>
<Shortcut key="Backspace" on:pressed="onGoBack()"/>
<style>
@ -37,25 +34,19 @@
text-overflow: ellipsis;
}
.dynamic-page-go-back {
display: inline-flex;
align-items: center;
justify-self: flex-end;
font-size: 1.2857142857142858em;
font-size: 1.3em;
color: var(--anchor-text);
border: 0;
padding: 0;
background: none;
justify-self: flex-end;
}
.dynamic-page-go-back:hover {
text-decoration: underline;
}
:global(.dynamic-page-go-back-icon) {
position: relative;
bottom: 0.06em;
margin-right: 0.2em;
height: 0.66666666em;
width: 0.66666666em;
fill: currentColor;
.dynamic-page-go-back::before {
content: '←';
margin-right: 5px;
}
@media (max-width: 767px) {
.dynamic-page-banner {

View file

@ -76,10 +76,6 @@
}
if (notificationType === 'admin.sign_up') {
return formatIntl('intl.accountSignedUp', params)
} else if (notificationType === 'follow_request') {
return formatIntl('intl.accountRequestedFollow', params)
} else if (notificationType === 'admin.report') {
return formatIntl('intl.accountReported', params)
} else { // 'follow'
return formatIntl('intl.accountFollowedYou', params)
}

View file

@ -139,10 +139,6 @@
return '#fa-user-plus'
} else if (notificationType === 'update') {
return '#fa-pencil'
} else if (notificationType === 'follow_request') {
return '#fa-hourglass'
} else if (notificationType === 'admin.report') {
return '#fa-flag'
}
return '#fa-star'
},
@ -167,10 +163,6 @@
return 'intl.reblogged'
} else if (notificationType === 'update') {
return 'intl.edited'
} else if (notificationType === 'follow_request') {
return 'intl.requestedFollow'
} else if (notificationType === 'admin.report') {
return 'intl.reported'
} else {
return ''
}

View file

@ -5,8 +5,6 @@ import {
} from '../__sapper__/service-worker.js'
import { get, post } from './routes/_utils/ajax.js'
import { setWebShareData, closeKeyValIDBConnection } from './routes/_database/webShare.js'
import { getKnownInstances } from './routes/_database/knownInstances.js'
import { basename } from './routes/_api/utils.js'
const timestamp = process.env.SAPPER_TIMESTAMP
const ASSETS = `assets_${timestamp}`
@ -171,18 +169,8 @@ self.addEventListener('fetch', event => {
self.addEventListener('push', event => {
event.waitUntil((async () => {
const data = event.data.json()
// If there is only once instance, then we know for sure that the push notification came from it
const knownInstances = await getKnownInstances()
if (knownInstances.length !== 1) {
// TODO: Mastodon currently does not tell us which instance the push notification came from.
// So we have to guess and currently just choose the first one. We _could_ locally store the instance that
// currently has push notifications enabled, but this would only work for one instance at a time.
// See: https://github.com/mastodon/mastodon/issues/22183
await showSimpleNotification(data)
return
}
const { origin } = event.target
const origin = basename(knownInstances[0])
try {
const notification = await get(`${origin}/api/v1/notifications/${data.notification_id}`, {
Authorization: `Bearer ${data.access_token}`
@ -215,8 +203,6 @@ async function showRichNotification (data, notification) {
switch (notification.type) {
case 'follow':
case 'follow_request':
case 'admin.report':
case 'admin.sign_up': {
await self.registration.showNotification(data.title, {
badge,

View file

@ -1 +1 @@
<svg viewBox="0 0 1792 1792" width="1792" height="1792" xmlns:svg="http://www.w3.org/2000/svg"><path fill="#fff" d="m 384.00001,344.0625 a 71.966714,71.966714 0 0 0 -56.18749,27 l -256.000008,320 a 71.966714,71.966714 0 0 0 56.187498,116.875 h 104.0625 V 1376 a 71.966714,71.966714 0 0 0 71.9375,71.9375 H 1024 a 71.966714,71.966714 0 0 0 56.1875,-116.875 l -128,-160 a 71.966714,71.966714 0 0 0 -56.18749,-27 h -360.0625 v -336.125 h 104.0625 a 71.966714,71.966714 0 0 0 56.18748,-116.875 l -256,-320 a 71.966714,71.966714 0 0 0 -56.18748,-27 z m 384,0 a 71.966714,71.966714 0 0 0 -56.18749,116.875 l 128,160 a 71.966714,71.966714 0 0 0 56.18749,27 h 360.06249 v 336.125 H 1152 a 71.966714,71.966714 0 0 0 -56.1875,116.875 l 256,320 a 71.966714,71.966714 0 0 0 112.375,0 l 256,-320 A 71.966714,71.966714 0 0 0 1664,984.0625 H 1559.9375 V 416 A 71.966714,71.966714 0 0 0 1488,344.0625 Z" /></svg>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 2048 1792"><path fill="#fff" d="M1344 1504q0 13-9 23t-23 9H352q-8 0-13-2t-9-7-6-8-3-11-1-12V896H128q-26 0-45-19t-19-45q0-24 15-41l320-384q19-22 49-22t49 22l320 384q15 17 15 41 0 26-19 45t-45 19H576v384h576q16 0 25 11l160 192q7 10 7 21zm640-416q0 24-15 41l-320 384q-20 23-49 23t-49-23l-320-384q-15-17-15-41 0-26 19-45t45-19h192V640H896q-16 0-25-12L711 436q-7-9-7-20 0-13 10-22t22-10h960q8 0 14 2t9 7 5 8 3 12 1 11v600h192q26 0 45 19t19 45z"/></svg>

Before

Width:  |  Height:  |  Size: 897 B

After

Width:  |  Height:  |  Size: 500 B

View file

@ -2,16 +2,8 @@ import { loginAsLockedAccount } from '../roles'
import { followAs, unfollowAs } from '../serverActions'
import {
avatarInComposeBox,
communityNavButton,
followersButton,
getNthSearchResult,
getNthStatus,
getSearchResultByHref,
getUrl,
goBack,
homeNavButton,
notificationsNavButton,
sleep
communityNavButton, followersButton, getNthSearchResult, getSearchResultByHref, getUrl, goBack,
homeNavButton, sleep
} from '../utils'
import { users } from '../users'
import { Selector as $ } from 'testcafe'
@ -101,9 +93,6 @@ test('Shows unresolved follow requests', async t => {
await t
.expect(communityNavButton.getAttribute('aria-label')).eql('Community (2 follow requests)')
.click(notificationsNavButton)
.expect(getUrl()).contains('/notifications')
.expect(getNthStatus(1).innerText).contains('requested to follow you')
.click(communityNavButton)
.expect(requestsButton.innerText).contains('Follow requests (2)')
.click(requestsButton)

View file

@ -4,10 +4,10 @@ import {
getNthPinnedStatusFavoriteButton,
getNthStatus, getNthStatusContent,
getNthStatusOptionsButton, getUrl, homeNavButton, postStatusButton, scrollToTop, scrollToBottom,
settingsNavButton, sleep, getNthStatusAccountLink
settingsNavButton, sleep
} from '../utils'
import { users } from '../users'
import { postAs, postStatusWithMediaAs } from '../serverActions'
import { postAs } from '../serverActions'
fixture`117-pin-unpin.js`
.page`http://localhost:4002`
@ -84,22 +84,3 @@ test('Saved pinned/unpinned state of status', async t => {
.click(getNthStatusOptionsButton(1))
.expect(getNthDialogOptionsOption(2).innerText).contains('Unpin from profile', { timeout })
})
test('pinned posts and aria-labels', async t => {
const timeout = 20000
await postStatusWithMediaAs('foobar', 'here is a sensitive kitty', 'kitten2.jpg', 'kitten', true)
await loginAsFoobar(t)
await t
.expect(getNthStatusContent(1).innerText).contains('here is a sensitive kitty', { timeout })
.click(getNthStatusOptionsButton(1))
.expect(getNthDialogOptionsOption(2).innerText).contains('Pin to profile')
.click(getNthDialogOptionsOption(2))
.click(getNthStatusAccountLink(1))
.expect(getNthPinnedStatus(1).getAttribute('aria-label')).match(
/foobar, here is a sensitive kitty, has media, (.+ ago|just now), @foobar, Public/i
)
.expect(getNthStatusContent(1).innerText).contains('here is a sensitive kitty')
.click(getNthStatusOptionsButton(1))
.expect(getNthDialogOptionsOption(2).innerText).contains('Unpin from profile')
await sleep(2000)
})