Merge remote-tracking branch 'pleroma/develop' into birthdays

Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
This commit is contained in:
marcin mikołajczak 2022-08-05 11:53:44 +02:00
commit 6649baaac9
316 changed files with 12331 additions and 6859 deletions

View file

@ -9,6 +9,8 @@ const FOLLOW_IMPORT_URL = '/api/pleroma/follow_import'
const DELETE_ACCOUNT_URL = '/api/pleroma/delete_account'
const CHANGE_EMAIL_URL = '/api/pleroma/change_email'
const CHANGE_PASSWORD_URL = '/api/pleroma/change_password'
const MOVE_ACCOUNT_URL = '/api/pleroma/move_account'
const ALIASES_URL = '/api/pleroma/aliases'
const TAG_USER_URL = '/api/pleroma/admin/users/tag'
const PERMISSION_GROUP_URL = (screenName, right) => `/api/pleroma/admin/users/${screenName}/permission_group/${right}`
const ACTIVATE_USER_URL = '/api/pleroma/admin/users/activate'
@ -74,7 +76,7 @@ const MASTODON_PIN_OWN_STATUS = id => `/api/v1/statuses/${id}/pin`
const MASTODON_UNPIN_OWN_STATUS = id => `/api/v1/statuses/${id}/unpin`
const MASTODON_MUTE_CONVERSATION = id => `/api/v1/statuses/${id}/mute`
const MASTODON_UNMUTE_CONVERSATION = id => `/api/v1/statuses/${id}/unmute`
const MASTODON_SEARCH_2 = `/api/v2/search`
const MASTODON_SEARCH_2 = '/api/v2/search'
const MASTODON_USER_SEARCH_URL = '/api/v1/accounts/search'
const MASTODON_DOMAIN_BLOCKS_URL = '/api/v1/domain_blocks'
const MASTODON_STREAMING = '/api/v1/streaming'
@ -82,15 +84,16 @@ const MASTODON_KNOWN_DOMAIN_LIST_URL = '/api/v1/instance/peers'
const PLEROMA_EMOJI_REACTIONS_URL = id => `/api/v1/pleroma/statuses/${id}/reactions`
const PLEROMA_EMOJI_REACT_URL = (id, emoji) => `/api/v1/pleroma/statuses/${id}/reactions/${emoji}`
const PLEROMA_EMOJI_UNREACT_URL = (id, emoji) => `/api/v1/pleroma/statuses/${id}/reactions/${emoji}`
const PLEROMA_CHATS_URL = `/api/v1/pleroma/chats`
const PLEROMA_CHATS_URL = '/api/v1/pleroma/chats'
const PLEROMA_CHAT_URL = id => `/api/v1/pleroma/chats/by-account-id/${id}`
const PLEROMA_CHAT_MESSAGES_URL = id => `/api/v1/pleroma/chats/${id}/messages`
const PLEROMA_CHAT_READ_URL = id => `/api/v1/pleroma/chats/${id}/read`
const PLEROMA_DELETE_CHAT_MESSAGE_URL = (chatId, messageId) => `/api/v1/pleroma/chats/${chatId}/messages/${messageId}`
const PLEROMA_BACKUP_URL = '/api/v1/pleroma/backups'
const oldfetch = window.fetch
let fetch = (url, options) => {
const fetch = (url, options) => {
options = options || {}
const baseUrl = ''
const fullUrl = baseUrl + url
@ -102,7 +105,7 @@ const promisedRequest = ({ method, url, params, payload, credentials, headers =
const options = {
method,
headers: {
'Accept': 'application/json',
Accept: 'application/json',
'Content-Type': 'application/json',
...headers
}
@ -151,9 +154,15 @@ const updateNotificationSettings = ({ credentials, settings }) => {
}).then((data) => data.json())
}
const updateProfileImages = ({ credentials, avatar = null, banner = null, background = null }) => {
const updateProfileImages = ({ credentials, avatar = null, avatarName = null, banner = null, background = null }) => {
const form = new FormData()
if (avatar !== null) form.append('avatar', avatar)
if (avatar !== null) {
if (avatarName !== null) {
form.append('avatar', avatar, avatarName)
} else {
form.append('avatar', avatar)
}
}
if (banner !== null) form.append('header', banner)
if (background !== null) form.append('pleroma_background_image', background)
return fetch(MASTODON_PROFILE_UPDATE_URL, {
@ -191,6 +200,7 @@ const updateProfile = ({ credentials, params }) => {
// homepage
// location
// token
// language
const register = ({ params, credentials }) => {
const { nickname, ...rest } = params
return fetch(MASTODON_REGISTRATION_URL, {
@ -219,16 +229,16 @@ const getCaptcha = () => fetch('/api/pleroma/captcha').then(resp => resp.json())
const authHeaders = (accessToken) => {
if (accessToken) {
return { 'Authorization': `Bearer ${accessToken}` }
return { Authorization: `Bearer ${accessToken}` }
} else {
return { }
}
}
const followUser = ({ id, credentials, ...options }) => {
let url = MASTODON_FOLLOW_URL(id)
const url = MASTODON_FOLLOW_URL(id)
const form = {}
if (options.reblogs !== undefined) { form['reblogs'] = options.reblogs }
if (options.reblogs !== undefined) { form.reblogs = options.reblogs }
return fetch(url, {
body: JSON.stringify(form),
headers: {
@ -240,7 +250,7 @@ const followUser = ({ id, credentials, ...options }) => {
}
const unfollowUser = ({ id, credentials }) => {
let url = MASTODON_UNFOLLOW_URL(id)
const url = MASTODON_UNFOLLOW_URL(id)
return fetch(url, {
headers: authHeaders(credentials),
method: 'POST'
@ -282,7 +292,7 @@ const unblockUser = ({ id, credentials }) => {
}
const approveUser = ({ id, credentials }) => {
let url = MASTODON_APPROVE_USER_URL(id)
const url = MASTODON_APPROVE_USER_URL(id)
return fetch(url, {
headers: authHeaders(credentials),
method: 'POST'
@ -290,7 +300,7 @@ const approveUser = ({ id, credentials }) => {
}
const denyUser = ({ id, credentials }) => {
let url = MASTODON_DENY_USER_URL(id)
const url = MASTODON_DENY_USER_URL(id)
return fetch(url, {
headers: authHeaders(credentials),
method: 'POST'
@ -298,13 +308,13 @@ const denyUser = ({ id, credentials }) => {
}
const fetchUser = ({ id, credentials }) => {
let url = `${MASTODON_USER_URL}/${id}`
const url = `${MASTODON_USER_URL}/${id}`
return promisedRequest({ url, credentials })
.then((data) => parseUser(data))
}
const fetchUserRelationship = ({ id, credentials }) => {
let url = `${MASTODON_USER_RELATIONSHIPS_URL}/?id=${id}`
const url = `${MASTODON_USER_RELATIONSHIPS_URL}/?id=${id}`
return fetch(url, { headers: authHeaders(credentials) })
.then((response) => {
return new Promise((resolve, reject) => response.json()
@ -323,7 +333,7 @@ const fetchFriends = ({ id, maxId, sinceId, limit = 20, credentials }) => {
maxId && `max_id=${maxId}`,
sinceId && `since_id=${sinceId}`,
limit && `limit=${limit}`,
`with_relationships=true`
'with_relationships=true'
].filter(_ => _).join('&')
url = url + (args ? '?' + args : '')
@ -333,6 +343,7 @@ const fetchFriends = ({ id, maxId, sinceId, limit = 20, credentials }) => {
}
const exportFriends = ({ id, credentials }) => {
// eslint-disable-next-line no-async-promise-executor
return new Promise(async (resolve, reject) => {
try {
let friends = []
@ -358,7 +369,7 @@ const fetchFollowers = ({ id, maxId, sinceId, limit = 20, credentials }) => {
maxId && `max_id=${maxId}`,
sinceId && `since_id=${sinceId}`,
limit && `limit=${limit}`,
`with_relationships=true`
'with_relationships=true'
].filter(_ => _).join('&')
url += args ? '?' + args : ''
@ -375,7 +386,7 @@ const fetchFollowRequests = ({ credentials }) => {
}
const fetchConversation = ({ id, credentials }) => {
let urlContext = MASTODON_STATUS_CONTEXT_URL(id)
const urlContext = MASTODON_STATUS_CONTEXT_URL(id)
return fetch(urlContext, { headers: authHeaders(credentials) })
.then((data) => {
if (data.ok) {
@ -391,7 +402,7 @@ const fetchConversation = ({ id, credentials }) => {
}
const fetchStatus = ({ id, credentials }) => {
let url = MASTODON_STATUS_URL(id)
const url = MASTODON_STATUS_URL(id)
return fetch(url, { headers: authHeaders(credentials) })
.then((data) => {
if (data.ok) {
@ -415,7 +426,7 @@ const tagUser = ({ tag, credentials, user }) => {
return fetch(TAG_USER_URL, {
method: 'PUT',
headers: headers,
headers,
body: JSON.stringify(form)
})
}
@ -432,7 +443,7 @@ const untagUser = ({ tag, credentials, user }) => {
return fetch(TAG_USER_URL, {
method: 'DELETE',
headers: headers,
headers,
body: JSON.stringify(body)
})
}
@ -485,7 +496,7 @@ const deleteUser = ({ credentials, user }) => {
return fetch(`${ADMIN_USERS_URL}?nickname=${screenName}`, {
method: 'DELETE',
headers: headers
headers
})
}
@ -504,7 +515,7 @@ const fetchTimeline = ({
friends: MASTODON_USER_HOME_TIMELINE_URL,
dms: MASTODON_DIRECT_MESSAGES_TIMELINE_URL,
notifications: MASTODON_USER_NOTIFICATIONS_URL,
'publicAndExternal': MASTODON_PUBLIC_TIMELINE,
publicAndExternal: MASTODON_PUBLIC_TIMELINE,
user: MASTODON_USER_TIMELINE_URL,
media: MASTODON_USER_TIMELINE_URL,
favorites: MASTODON_USER_FAVORITES_TIMELINE_URL,
@ -678,7 +689,7 @@ const postStatus = ({
form.append('preview', 'true')
}
let postHeaders = authHeaders(credentials)
const postHeaders = authHeaders(credentials)
if (idempotencyKey) {
postHeaders['idempotency-key'] = idempotencyKey
}
@ -782,6 +793,49 @@ const changeEmail = ({ credentials, email, password }) => {
.then((response) => response.json())
}
const moveAccount = ({ credentials, password, targetAccount }) => {
const form = new FormData()
form.append('password', password)
form.append('target_account', targetAccount)
return fetch(MOVE_ACCOUNT_URL, {
body: form,
method: 'POST',
headers: authHeaders(credentials)
})
.then((response) => response.json())
}
const addAlias = ({ credentials, alias }) => {
return promisedRequest({
url: ALIASES_URL,
method: 'PUT',
credentials,
payload: { alias }
})
}
const deleteAlias = ({ credentials, alias }) => {
return promisedRequest({
url: ALIASES_URL,
method: 'DELETE',
credentials,
payload: { alias }
})
}
const listAliases = ({ credentials }) => {
return promisedRequest({
url: ALIASES_URL,
method: 'GET',
credentials,
params: {
_cacheBooster: (new Date()).getTime()
}
})
}
const changePassword = ({ credentials, password, newPassword, newPasswordConfirmation }) => {
const form = new FormData()
@ -868,6 +922,25 @@ const fetchBlocks = ({ credentials }) => {
.then((users) => users.map(parseUser))
}
const addBackup = ({ credentials }) => {
return promisedRequest({
url: PLEROMA_BACKUP_URL,
method: 'POST',
credentials
})
}
const listBackups = ({ credentials }) => {
return promisedRequest({
url: PLEROMA_BACKUP_URL,
method: 'GET',
credentials,
params: {
_cacheBooster: (new Date()).getTime()
}
})
}
const fetchOAuthTokens = ({ credentials }) => {
const url = '/api/oauth_tokens.json'
@ -921,7 +994,7 @@ const vote = ({ pollId, choices, credentials }) => {
method: 'POST',
credentials,
payload: {
choices: choices
choices
}
})
}
@ -981,8 +1054,8 @@ const reportUser = ({ credentials, userId, statusIds, comment, forward }) => {
url: MASTODON_REPORT_USER_URL,
method: 'POST',
payload: {
'account_id': userId,
'status_ids': statusIds,
account_id: userId,
status_ids: statusIds,
comment,
forward
},
@ -1004,7 +1077,7 @@ const searchUsers = ({ credentials, query }) => {
const search2 = ({ credentials, q, resolve, limit, offset, following }) => {
let url = MASTODON_SEARCH_2
let params = []
const params = []
if (q) {
params.push(['q', encodeURIComponent(q)])
@ -1028,7 +1101,7 @@ const search2 = ({ credentials, q, resolve, limit, offset, following }) => {
params.push(['with_relationships', true])
let queryString = map(params, (param) => `${param[0]}=${param[1]}`).join('&')
const queryString = map(params, (param) => `${param[0]}=${param[1]}`).join('&')
url += `?${queryString}`
return fetch(url, { headers: authHeaders(credentials) })
@ -1182,12 +1255,12 @@ export const handleMastoWS = (wsEvent) => {
}
export const WSConnectionStatus = Object.freeze({
'JOINED': 1,
'CLOSED': 2,
'ERROR': 3,
'DISABLED': 4,
'STARTING': 5,
'STARTING_INITIAL': 6
JOINED: 1,
CLOSED: 2,
ERROR: 3,
DISABLED: 4,
STARTING: 5,
STARTING_INITIAL: 6
})
const chats = ({ credentials }) => {
@ -1225,11 +1298,11 @@ const chatMessages = ({ id, credentials, maxId, sinceId, limit = 20 }) => {
const sendChatMessage = ({ id, content, mediaId = null, idempotencyKey, credentials }) => {
const payload = {
'content': content
content
}
if (mediaId) {
payload['media_id'] = mediaId
payload.media_id = mediaId
}
const headers = {}
@ -1241,7 +1314,7 @@ const sendChatMessage = ({ id, content, mediaId = null, idempotencyKey, credenti
return promisedRequest({
url: PLEROMA_CHAT_MESSAGES_URL(id),
method: 'POST',
payload: payload,
payload,
credentials,
headers
})
@ -1252,7 +1325,7 @@ const readChat = ({ id, lastReadId, credentials }) => {
url: PLEROMA_CHAT_READ_URL(id),
method: 'POST',
payload: {
'last_read_id': lastReadId
last_read_id: lastReadId
},
credentials
})
@ -1319,12 +1392,18 @@ const apiService = {
importFollows,
deleteAccount,
changeEmail,
moveAccount,
addAlias,
deleteAlias,
listAliases,
changePassword,
settingsMFA,
mfaDisableOTP,
generateMfaBackupCodes,
mfaSetupOTP,
mfaConfirmOTP,
addBackup,
listBackups,
fetchFollowRequests,
approveUser,
denyUser,

View file

@ -7,7 +7,7 @@ const empty = (chatId) => {
messages: [],
newMessageCount: 0,
lastSeenMessageId: '0',
chatId: chatId,
chatId,
minId: undefined,
maxId: undefined
}
@ -101,7 +101,7 @@ const add = (storage, { messages: newMessages, updateMaxId = true }) => {
storage.messages = storage.messages.filter(msg => msg.id !== message.id)
}
Object.assign(fakeMessage, message, { error: false })
delete fakeMessage['fakeId']
delete fakeMessage.fakeId
storage.idIndex[fakeMessage.id] = fakeMessage
delete storage.idIndex[message.fakeId]
@ -178,7 +178,7 @@ const getView = (storage) => {
id: date.getTime().toString()
})
previousMessage['isTail'] = true
previousMessage.isTail = true
currentMessageChainId = undefined
afterDate = true
}
@ -193,15 +193,15 @@ const getView = (storage) => {
// end a message chian
if ((nextMessage && nextMessage.account_id) !== message.account_id) {
object['isTail'] = true
object.isTail = true
currentMessageChainId = undefined
}
// start a new message chain
if ((previousMessage && previousMessage.data && previousMessage.data.account_id) !== message.account_id || afterDate) {
currentMessageChainId = _.uniqueId()
object['isHead'] = true
object['messageChainId'] = currentMessageChainId
object.isHead = true
object.messageChainId = currentMessageChainId
}
result.push(object)

View file

@ -25,7 +25,7 @@ export const buildFakeMessage = ({ content, chatId, attachments, userId, idempot
chat_id: chatId,
created_at: new Date(),
id: `${new Date().getTime()}`,
attachments: attachments,
attachments,
account_id: userId,
idempotency_key: idempotencyKey,
emojis: [],

View file

@ -144,11 +144,13 @@ export const invert = (rgb) => {
*/
export const hex2rgb = (hex) => {
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex)
return result ? {
r: parseInt(result[1], 16),
g: parseInt(result[2], 16),
b: parseInt(result[3], 16)
} : null
return result
? {
r: parseInt(result[1], 16),
g: parseInt(result[2], 16),
b: parseInt(result[3], 16)
}
: null
}
/**

View file

@ -35,7 +35,7 @@ export const addPositionToWords = (words) => {
}
export const splitByWhitespaceBoundary = (str) => {
let result = []
const result = []
let currentWord = ''
for (let i = 0; i < str.length; i++) {
const currentChar = str[i]

View file

@ -10,31 +10,29 @@ export const relativeTime = (date, nowThreshold = 1) => {
if (typeof date === 'string') date = Date.parse(date)
const round = Date.now() > date ? Math.floor : Math.ceil
const d = Math.abs(Date.now() - date)
let r = { num: round(d / YEAR), key: 'time.years' }
const r = { num: round(d / YEAR), key: 'time.unit.years' }
if (d < nowThreshold * SECOND) {
r.num = 0
r.key = 'time.now'
} else if (d < MINUTE) {
r.num = round(d / SECOND)
r.key = 'time.seconds'
r.key = 'time.unit.seconds'
} else if (d < HOUR) {
r.num = round(d / MINUTE)
r.key = 'time.minutes'
r.key = 'time.unit.minutes'
} else if (d < DAY) {
r.num = round(d / HOUR)
r.key = 'time.hours'
r.key = 'time.unit.hours'
} else if (d < WEEK) {
r.num = round(d / DAY)
r.key = 'time.days'
r.key = 'time.unit.days'
} else if (d < MONTH) {
r.num = round(d / WEEK)
r.key = 'time.weeks'
r.key = 'time.unit.weeks'
} else if (d < YEAR) {
r.num = round(d / MONTH)
r.key = 'time.months'
r.key = 'time.unit.months'
}
// Remove plural form when singular
if (r.num === 1) r.key = r.key.slice(0, -1)
return r
}

View file

@ -39,11 +39,12 @@ const qvitterStatusType = (status) => {
export const parseUser = (data) => {
const output = {}
const masto = data.hasOwnProperty('acct')
const masto = Object.prototype.hasOwnProperty.call(data, 'acct')
// case for users in "mentions" property for statuses in MastoAPI
const mastoShort = masto && !data.hasOwnProperty('avatar')
const mastoShort = masto && !Object.prototype.hasOwnProperty.call(data, 'avatar')
output.id = String(data.id)
output._original = data // used for server-side settings
if (masto) {
output.screen_name = data.acct
@ -227,7 +228,7 @@ export const parseUser = (data) => {
export const parseAttachment = (data) => {
const output = {}
const masto = !data.hasOwnProperty('oembed')
const masto = !Object.prototype.hasOwnProperty.call(data, 'oembed')
if (masto) {
// Not exactly same...
@ -248,7 +249,7 @@ export const parseAttachment = (data) => {
export const parseStatus = (data) => {
const output = {}
const masto = data.hasOwnProperty('account')
const masto = Object.prototype.hasOwnProperty.call(data, 'account')
if (masto) {
output.favorited = data.favourited
@ -373,10 +374,10 @@ export const parseStatus = (data) => {
export const parseNotification = (data) => {
const mastoDict = {
'favourite': 'like',
'reblog': 'repeat'
favourite: 'like',
reblog: 'repeat'
}
const masto = !data.hasOwnProperty('ntype')
const masto = !Object.prototype.hasOwnProperty.call(data, 'ntype')
const output = {}
if (masto) {

View file

@ -26,6 +26,7 @@ export class RegistrationError extends Error {
// the error is probably a JSON object with a single key, "errors", whose value is another JSON object containing the real errors
if (typeof error === 'string') {
error = JSON.parse(error)
// eslint-disable-next-line
if (error.hasOwnProperty('error')) {
error = JSON.parse(error.error)
}

View file

@ -1,9 +1,11 @@
import utf8 from 'utf8'
export const newExporter = ({
filename = 'data',
getExportedObject
}) => ({
exportData () {
const stringified = JSON.stringify(getExportedObject(), null, 2) // Pretty-print and indent with 2 spaces
const stringified = utf8.encode(JSON.stringify(getExportedObject(), null, 2)) // Pretty-print and indent with 2 spaces
// Create an invisible link with a data url and simulate a click
const e = document.createElement('a')

View file

@ -1,15 +1,14 @@
const fileSizeFormat = (num) => {
var exponent
var unit
var units = ['B', 'KiB', 'MiB', 'GiB', 'TiB']
const fileSizeFormat = (numArg) => {
const units = ['B', 'KiB', 'MiB', 'GiB', 'TiB']
let num = numArg
if (num < 1) {
return num + ' ' + units[0]
}
exponent = Math.min(Math.floor(Math.log(num) / Math.log(1024)), units.length - 1)
const exponent = Math.min(Math.floor(Math.log(num) / Math.log(1024)), units.length - 1)
num = (num / Math.pow(1024, exponent)).toFixed(2) * 1
unit = units[exponent]
return { num: num, unit: unit }
const unit = units[exponent]
return { num, unit }
}
const fileSizeFormatService = {
fileSizeFormat

View file

@ -4,9 +4,15 @@ const DIRECTION_RIGHT = [1, 0]
const DIRECTION_UP = [0, -1]
const DIRECTION_DOWN = [0, 1]
const BUTTON_LEFT = 0
const deltaCoord = (oldCoord, newCoord) => [newCoord[0] - oldCoord[0], newCoord[1] - oldCoord[1]]
const touchEventCoord = e => ([e.touches[0].screenX, e.touches[0].screenY])
const touchCoord = touch => [touch.screenX, touch.screenY]
const touchEventCoord = e => touchCoord(e.touches[0])
const pointerEventCoord = e => [e.clientX, e.clientY]
const vectorLength = v => Math.sqrt(v[0] * v[0] + v[1] * v[1])
@ -61,6 +67,132 @@ const updateSwipe = (event, gesture) => {
gesture._swiping = false
}
class SwipeAndClickGesture {
// swipePreviewCallback(offsets: Array[Number])
// offsets: the offset vector which the underlying component should move, from the starting position
// swipeEndCallback(sign: 0|-1|1)
// sign: if the swipe does not meet the threshold, 0
// if the swipe meets the threshold in the positive direction, 1
// if the swipe meets the threshold in the negative direction, -1
constructor ({
direction,
// swipeStartCallback
swipePreviewCallback,
swipeEndCallback,
swipeCancelCallback,
swipelessClickCallback,
threshold = 30,
perpendicularTolerance = 1.0,
disableClickThreshold = 1
}) {
const nop = () => {}
this.direction = direction
this.swipePreviewCallback = swipePreviewCallback || nop
this.swipeEndCallback = swipeEndCallback || nop
this.swipeCancelCallback = swipeCancelCallback || nop
this.swipelessClickCallback = swipelessClickCallback || nop
this.threshold = typeof threshold === 'function' ? threshold : () => threshold
this.disableClickThreshold = typeof disableClickThreshold === 'function' ? disableClickThreshold : () => disableClickThreshold
this.perpendicularTolerance = perpendicularTolerance
this._reset()
}
_reset () {
this._startPos = [0, 0]
this._pointerId = -1
this._swiping = false
this._swiped = false
this._preventNextClick = false
}
start (event) {
// Only handle left click
if (event.button !== BUTTON_LEFT) {
return
}
this._startPos = pointerEventCoord(event)
this._pointerId = event.pointerId
this._swiping = true
this._swiped = false
}
move (event) {
if (this._swiping && this._pointerId === event.pointerId) {
this._swiped = true
const coord = pointerEventCoord(event)
const delta = deltaCoord(this._startPos, coord)
this.swipePreviewCallback(delta)
}
}
cancel (event) {
if (!this._swiping || this._pointerId !== event.pointerId) {
return
}
this.swipeCancelCallback()
}
end (event) {
if (!this._swiping) {
return
}
if (this._pointerId !== event.pointerId) {
return
}
this._swiping = false
// movement too small
const coord = pointerEventCoord(event)
const delta = deltaCoord(this._startPos, coord)
const sign = (() => {
if (vectorLength(delta) < this.threshold()) {
return 0
}
// movement is opposite from direction
const isPositive = dotProduct(delta, this.direction) > 0
// movement perpendicular to direction is too much
const towardsDir = project(delta, this.direction)
const perpendicularDir = perpendicular(this.direction)
const towardsPerpendicular = project(delta, perpendicularDir)
if (
vectorLength(towardsDir) * this.perpendicularTolerance <
vectorLength(towardsPerpendicular)
) {
return 0
}
return isPositive ? 1 : -1
})()
if (this._swiped) {
this.swipeEndCallback(sign)
}
this._reset()
// Only a mouse will fire click event when
// the end point is far from the starting point
// so for other kinds of pointers do not check
// whether we have swiped
if (vectorLength(delta) >= this.disableClickThreshold() && event.pointerType === 'mouse') {
this._preventNextClick = true
}
}
click (event) {
if (!this._preventNextClick) {
this.swipelessClickCallback()
}
this._reset()
}
}
const GestureService = {
DIRECTION_LEFT,
DIRECTION_RIGHT,
@ -68,7 +200,8 @@ const GestureService = {
DIRECTION_DOWN,
swipeGesture,
beginSwipe,
updateSwipe
updateSwipe,
SwipeAndClickGesture
}
export default GestureService

View file

@ -46,7 +46,7 @@ export const convertHtmlToLines = (html = '') => {
// All block-level elements that aren't empty elements, i.e. not <hr>
const nonEmptyElements = new Set(visualLineElements)
// Difference
for (let elem of emptyElements) {
for (const elem of emptyElements) {
nonEmptyElements.delete(elem)
}
@ -56,7 +56,7 @@ export const convertHtmlToLines = (html = '') => {
...emptyElements.values()
])
let buffer = [] // Current output buffer
const buffer = [] // Current output buffer
const level = [] // How deep we are in tags and which tags were there
let textBuffer = '' // Current line content
let tagBuffer = null // Current tag buffer, if null = we are not currently reading a tag

View file

@ -1,4 +1,5 @@
import { getTagName } from './utility.service.js'
import { unescape } from 'lodash'
/**
* This is a not-so-tiny purpose-built HTML parser/processor. This parses html
@ -49,7 +50,7 @@ export const convertHtmlToTree = (html = '') => {
const handleOpen = (tag) => {
const curBuf = getCurrentBuffer()
const newLevel = [tag, []]
const newLevel = [unescape(tag), []]
levels.push(newLevel)
curBuf.push(newLevel)
}

View file

@ -50,7 +50,7 @@ export const processTextForEmoji = (text, emojis, processor) => {
if (char === ':') {
const next = text.slice(i + 1)
let found = false
for (let emoji of emojis) {
for (const emoji of emojis) {
if (next.slice(0, emoji.shortcode.length + 1) === (emoji.shortcode + ':')) {
found = emoji
break

View file

@ -1,12 +1,35 @@
import languagesObject from '../../i18n/messages'
import ISO6391 from 'iso-639-1'
import _ from 'lodash'
const specialLanguageCodes = {
'ja_easy': 'ja',
'zh_Hant': 'zh-HANT'
ja_easy: 'ja',
zh_Hant: 'zh-HANT',
zh: 'zh-Hans'
}
const internalToBrowserLocale = code => specialLanguageCodes[code] || code
const internalToBackendLocale = code => internalToBrowserLocale(code).replace('_', '-')
const getLanguageName = (code) => {
const specialLanguageNames = {
ja_easy: 'やさしいにほんご',
zh: '简体中文',
zh_Hant: '繁體中文'
}
const languageName = specialLanguageNames[code] || ISO6391.getNativeName(code)
const browserLocale = internalToBrowserLocale(code)
return languageName.charAt(0).toLocaleUpperCase(browserLocale) + languageName.slice(1)
}
const languages = _.map(languagesObject.languages, (code) => ({ code, name: getLanguageName(code) })).sort((a, b) => a.name.localeCompare(b.name))
const localeService = {
internalToBrowserLocale
internalToBrowserLocale,
internalToBackendLocale,
languages,
getLanguageName
}
export default localeService

View file

@ -1,6 +1,6 @@
import { reduce } from 'lodash'
const MASTODON_PASSWORD_RESET_URL = `/auth/password`
const MASTODON_PASSWORD_RESET_URL = '/auth/password'
const resetPassword = ({ instance, email }) => {
const params = { email }

View file

@ -14,11 +14,12 @@ export const visibleTypes = store => {
rootState.config.notificationVisibility.follows && 'follow',
rootState.config.notificationVisibility.followRequest && 'follow_request',
rootState.config.notificationVisibility.moves && 'move',
rootState.config.notificationVisibility.emojiReactions && 'pleroma:emoji_reaction'
rootState.config.notificationVisibility.emojiReactions && 'pleroma:emoji_reaction',
rootState.config.notificationVisibility.polls && 'poll'
].filter(_ => _))
}
const statusNotifications = ['like', 'mention', 'repeat', 'pleroma:emoji_reaction']
const statusNotifications = ['like', 'mention', 'repeat', 'pleroma:emoji_reaction', 'poll']
export const isStatusNotification = (type) => includes(statusNotifications, type)
@ -98,6 +99,9 @@ export const prepareNotificationObject = (notification, i18n) => {
case 'follow_request':
i18nString = 'follow_request'
break
case 'poll':
i18nString = 'poll_ended'
break
}
if (notification.type === 'pleroma:emoji_reaction') {

View file

@ -12,20 +12,20 @@ const fetchAndUpdate = ({ store, credentials, older = false, since }) => {
const timelineData = rootState.statuses.notifications
const hideMutedPosts = getters.mergedConfig.hideMutedPosts
args['withMuted'] = !hideMutedPosts
args.withMuted = !hideMutedPosts
args['timeline'] = 'notifications'
args.timeline = 'notifications'
if (older) {
if (timelineData.minId !== Number.POSITIVE_INFINITY) {
args['until'] = timelineData.minId
args.until = timelineData.minId
}
return fetchNotifications({ store, args, older })
} else {
// fetch new notifications
if (since === undefined && timelineData.maxId !== Number.POSITIVE_INFINITY) {
args['since'] = timelineData.maxId
args.since = timelineData.maxId
} else if (since !== null) {
args['since'] = since
args.since = since
}
const result = fetchNotifications({ store, args, older })
@ -38,7 +38,7 @@ const fetchAndUpdate = ({ store, credentials, older = false, since }) => {
const readNotifsIds = notifications.filter(n => n.seen).map(n => n.id)
const numUnseenNotifs = notifications.length - readNotifsIds.length
if (numUnseenNotifs > 0 && readNotifsIds.length > 0) {
args['since'] = Math.max(...readNotifsIds)
args.since = Math.max(...readNotifsIds)
fetchNotifications({ store, args, older })
}

View file

@ -9,7 +9,7 @@ export const findOffset = (child, parent, { top = 0, left = 0 } = {}, ignorePadd
result.left += ignorePadding ? 0 : leftPadding
}
if (child.offsetParent && (parent === window || parent.contains(child.offsetParent) || parent === child.offsetParent)) {
if (child.offsetParent && window.getComputedStyle(child.offsetParent).position !== 'sticky' && (parent === window || parent.contains(child.offsetParent) || parent === child.offsetParent)) {
return findOffset(child.offsetParent, parent, result, false)
} else {
if (parent !== window) {

View file

@ -43,7 +43,7 @@ function deleteSubscriptionFromBackEnd (token) {
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
Authorization: `Bearer ${token}`
}
}).then((response) => {
if (!response.ok) throw new Error('Bad status code from server.')
@ -56,7 +56,7 @@ function sendSubscriptionToBackEnd (subscription, token, notificationVisibility)
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
Authorization: `Bearer ${token}`
},
body: JSON.stringify({
subscription,

View file

@ -1,4 +1,4 @@
import Vue from 'vue'
import { defineAsyncComponent, shallowReactive, h } from 'vue'
/* By default async components don't have any way to recover, if component is
* failed, it is failed forever. This helper tries to remedy that by recreating
@ -8,23 +8,21 @@ import Vue from 'vue'
* actual target component itself if needs to be.
*/
function getResettableAsyncComponent (asyncComponent, options) {
const asyncComponentFactory = () => () => ({
component: asyncComponent(),
const asyncComponentFactory = () => () => defineAsyncComponent({
loader: asyncComponent,
...options
})
const observe = Vue.observable({ c: asyncComponentFactory() })
const observe = shallowReactive({ c: asyncComponentFactory() })
return {
functional: true,
render (createElement, { data, children }) {
render () {
// emit event resetAsyncComponent to reloading
data.on = {}
data.on.resetAsyncComponent = () => {
observe.c = asyncComponentFactory()
// parent.$forceUpdate()
}
return createElement(observe.c, data, children)
return h(observe.c(), {
onResetAsyncComponent () {
observe.c = asyncComponentFactory()
}
})
}
}
}

View file

@ -13,10 +13,10 @@ export const applyTheme = (input) => {
const styleSheet = styleEl.sheet
styleSheet.toString()
styleSheet.insertRule(`body { ${rules.radii} }`, 'index-max')
styleSheet.insertRule(`body { ${rules.colors} }`, 'index-max')
styleSheet.insertRule(`body { ${rules.shadows} }`, 'index-max')
styleSheet.insertRule(`body { ${rules.fonts} }`, 'index-max')
styleSheet.insertRule(`:root { ${rules.radii} }`, 'index-max')
styleSheet.insertRule(`:root { ${rules.colors} }`, 'index-max')
styleSheet.insertRule(`:root { ${rules.shadows} }`, 'index-max')
styleSheet.insertRule(`:root { ${rules.fonts} }`, 'index-max')
body.classList.remove('hidden')
}

View file

@ -39,7 +39,7 @@ import { LAYERS, DEFAULT_OPACITY, SLOT_INHERITANCE } from './pleromafe.js'
export const CURRENT_VERSION = 3
export const getLayersArray = (layer, data = LAYERS) => {
let array = [layer]
const array = [layer]
let parent = data[layer]
while (parent) {
array.unshift(parent)
@ -138,6 +138,7 @@ export const topoSort = (
if (depsA === depsB || (depsB !== 0 && depsA !== 0)) return ai - bi
if (depsA === 0 && depsB !== 0) return -1
if (depsB === 0 && depsA !== 0) return 1
return 0 // failsafe, shouldn't happen?
}).map(({ data }) => data)
}

View file

@ -34,20 +34,20 @@ const fetchAndUpdate = ({
const loggedIn = !!rootState.users.currentUser
if (older) {
args['until'] = until || timelineData.minId
args.until = until || timelineData.minId
} else {
if (since === undefined) {
args['since'] = timelineData.maxId
args.since = timelineData.maxId
} else if (since !== null) {
args['since'] = since
args.since = since
}
}
args['userId'] = userId
args['tag'] = tag
args['withMuted'] = !hideMutedPosts
args.userId = userId
args.tag = tag
args.withMuted = !hideMutedPosts
if (loggedIn && ['friends', 'public', 'publicAndExternal'].includes(timeline)) {
args['replyVisibility'] = replyVisibility
args.replyVisibility = replyVisibility
}
const numStatusesBeforeFetch = timelineData.statuses.length
@ -60,7 +60,7 @@ const fetchAndUpdate = ({
const { data: statuses, pagination } = response
if (!older && statuses.length >= 20 && !timelineData.loading && numStatusesBeforeFetch > 0) {
store.dispatch('queueFlush', { timeline: timeline, id: timelineData.maxId })
store.dispatch('queueFlush', { timeline, id: timelineData.maxId })
}
update({ store, statuses, timeline, showImmediately, userId, pagination })
return { statuses, pagination }

View file

@ -36,7 +36,7 @@ const highlightStyle = (prefs) => {
'linear-gradient(to right,',
`${solidColor} ,`,
`${solidColor} 2px,`,
`transparent 6px`
'transparent 6px'
].join(' '),
backgroundPosition: '0 0',
...customProps