Merge branch 'main' into web-layout
@@ -1,5 +1,6 @@
|
|||||||
import {resolveConfig} from 'detox/internals'
|
import {resolveConfig} from 'detox/internals'
|
||||||
import {execSync} from 'child_process'
|
import {execSync} from 'child_process'
|
||||||
|
import http from 'http'
|
||||||
|
|
||||||
const platform = device.getPlatform()
|
const platform = device.getPlatform()
|
||||||
|
|
||||||
@@ -105,9 +106,29 @@ async function openAppForDebugBuild(platform: string, opts: any) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function createServer(path = '') {
|
export async function createServer(path = '') {
|
||||||
const res = await fetch(`http://localhost:1986/${path}`, {method: 'POST'})
|
return new Promise(function (resolve, reject) {
|
||||||
const resBody = await res.text()
|
var req = http.request(
|
||||||
return resBody
|
{
|
||||||
|
method: 'POST',
|
||||||
|
host: 'localhost',
|
||||||
|
port: 1986,
|
||||||
|
path: `/${path}`,
|
||||||
|
},
|
||||||
|
function (res) {
|
||||||
|
const body: Buffer[] = []
|
||||||
|
res.on('data', chunk => body.push(chunk))
|
||||||
|
res.on('end', function () {
|
||||||
|
try {
|
||||||
|
resolve(Buffer.concat(body).toString())
|
||||||
|
} catch (e) {
|
||||||
|
reject(e)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
)
|
||||||
|
req.on('error', reject)
|
||||||
|
req.end()
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const getDeepLinkUrl = (url: string) =>
|
const getDeepLinkUrl = (url: string) =>
|
||||||
|
|||||||
@@ -394,6 +394,7 @@ describe('parseEmbedPlayerFromUrl', () => {
|
|||||||
'https://youtube.com/watch?v=videoId',
|
'https://youtube.com/watch?v=videoId',
|
||||||
'https://youtube.com/watch?v=videoId&feature=share',
|
'https://youtube.com/watch?v=videoId&feature=share',
|
||||||
'https://youtube.com/shorts/videoId',
|
'https://youtube.com/shorts/videoId',
|
||||||
|
'https://m.youtube.com/watch?v=videoId',
|
||||||
|
|
||||||
'https://youtube.com/shorts/',
|
'https://youtube.com/shorts/',
|
||||||
'https://youtube.com/',
|
'https://youtube.com/',
|
||||||
@@ -401,113 +402,354 @@ describe('parseEmbedPlayerFromUrl', () => {
|
|||||||
|
|
||||||
'https://twitch.tv/channelName',
|
'https://twitch.tv/channelName',
|
||||||
'https://www.twitch.tv/channelName',
|
'https://www.twitch.tv/channelName',
|
||||||
|
'https://m.twitch.tv/channelName',
|
||||||
|
|
||||||
|
'https://twitch.tv/channelName/clip/clipId',
|
||||||
|
'https://twitch.tv/videos/videoId',
|
||||||
|
|
||||||
'https://open.spotify.com/playlist/playlistId',
|
'https://open.spotify.com/playlist/playlistId',
|
||||||
'https://open.spotify.com/playlist/playlistId?param=value',
|
'https://open.spotify.com/playlist/playlistId?param=value',
|
||||||
|
'https://open.spotify.com/locale/playlist/playlistId',
|
||||||
|
|
||||||
'https://open.spotify.com/track/songId',
|
'https://open.spotify.com/track/songId',
|
||||||
'https://open.spotify.com/track/songId?param=value',
|
'https://open.spotify.com/track/songId?param=value',
|
||||||
|
'https://open.spotify.com/locale/track/songId',
|
||||||
|
|
||||||
'https://open.spotify.com/album/albumId',
|
'https://open.spotify.com/album/albumId',
|
||||||
'https://open.spotify.com/album/albumId?param=value',
|
'https://open.spotify.com/album/albumId?param=value',
|
||||||
|
'https://open.spotify.com/locale/album/albumId',
|
||||||
|
|
||||||
'https://soundcloud.com/user/track',
|
'https://soundcloud.com/user/track',
|
||||||
'https://soundcloud.com/user/sets/set',
|
'https://soundcloud.com/user/sets/set',
|
||||||
'https://soundcloud.com/user/',
|
'https://soundcloud.com/user/',
|
||||||
|
|
||||||
|
'https://music.apple.com/us/playlist/playlistName/playlistId',
|
||||||
|
'https://music.apple.com/us/album/albumName/albumId',
|
||||||
|
'https://music.apple.com/us/album/albumName/albumId?i=songId',
|
||||||
|
|
||||||
|
'https://vimeo.com/videoId',
|
||||||
|
'https://vimeo.com/videoId?autoplay=0',
|
||||||
|
|
||||||
|
'https://giphy.com/gifs/some-random-gif-name-gifId',
|
||||||
|
'https://giphy.com/gif/some-random-gif-name-gifId',
|
||||||
|
'https://giphy.com/gifs/',
|
||||||
|
|
||||||
|
'https://media.giphy.com/media/gifId/giphy.webp',
|
||||||
|
'https://media0.giphy.com/media/gifId/giphy.webp',
|
||||||
|
'https://media1.giphy.com/media/gifId/giphy.gif',
|
||||||
|
'https://media2.giphy.com/media/gifId/giphy.webp',
|
||||||
|
'https://media3.giphy.com/media/gifId/giphy.mp4',
|
||||||
|
'https://media4.giphy.com/media/gifId/giphy.webp',
|
||||||
|
'https://media5.giphy.com/media/gifId/giphy.mp4',
|
||||||
|
'https://media0.giphy.com/media/gifId/giphy.mp3',
|
||||||
|
'https://media1.google.com/media/gifId/giphy.webp',
|
||||||
|
|
||||||
|
'https://media.giphy.com/media/trackingId/gifId/giphy.webp',
|
||||||
|
|
||||||
|
'https://i.giphy.com/media/gifId/giphy.webp',
|
||||||
|
'https://i.giphy.com/media/gifId/giphy.webp',
|
||||||
|
'https://i.giphy.com/gifId.gif',
|
||||||
|
'https://i.giphy.com/gifId.gif',
|
||||||
|
|
||||||
|
'https://tenor.com/view/gifId',
|
||||||
|
'https://tenor.com/notView/gifId',
|
||||||
|
'https://tenor.com/view',
|
||||||
|
'https://tenor.com/view/gifId.gif',
|
||||||
|
'https://tenor.com/intl/view/gifId.gif',
|
||||||
]
|
]
|
||||||
|
|
||||||
const outputs = [
|
const outputs = [
|
||||||
{
|
{
|
||||||
type: 'youtube_video',
|
type: 'youtube_video',
|
||||||
videoId: 'videoId',
|
source: 'youtube',
|
||||||
playerUri: 'https://www.youtube.com/embed/videoId?autoplay=1',
|
playerUri:
|
||||||
|
'https://www.youtube.com/embed/videoId?autoplay=1&playsinline=1',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 'youtube_video',
|
type: 'youtube_video',
|
||||||
videoId: 'videoId',
|
source: 'youtube',
|
||||||
playerUri: 'https://www.youtube.com/embed/videoId?autoplay=1',
|
playerUri:
|
||||||
|
'https://www.youtube.com/embed/videoId?autoplay=1&playsinline=1',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 'youtube_video',
|
type: 'youtube_video',
|
||||||
videoId: 'videoId',
|
source: 'youtube',
|
||||||
playerUri: 'https://www.youtube.com/embed/videoId?autoplay=1',
|
playerUri:
|
||||||
|
'https://www.youtube.com/embed/videoId?autoplay=1&playsinline=1',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 'youtube_video',
|
type: 'youtube_video',
|
||||||
videoId: 'videoId',
|
source: 'youtube',
|
||||||
playerUri: 'https://www.youtube.com/embed/videoId?autoplay=1',
|
playerUri:
|
||||||
|
'https://www.youtube.com/embed/videoId?autoplay=1&playsinline=1',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 'youtube_video',
|
type: 'youtube_video',
|
||||||
videoId: 'videoId',
|
source: 'youtube',
|
||||||
playerUri: 'https://www.youtube.com/embed/videoId?autoplay=1',
|
playerUri:
|
||||||
|
'https://www.youtube.com/embed/videoId?autoplay=1&playsinline=1',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'youtube_short',
|
||||||
|
source: 'youtubeShorts',
|
||||||
|
hideDetails: true,
|
||||||
|
playerUri:
|
||||||
|
'https://www.youtube.com/embed/videoId?autoplay=1&playsinline=1',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 'youtube_video',
|
type: 'youtube_video',
|
||||||
videoId: 'videoId',
|
source: 'youtube',
|
||||||
playerUri: 'https://www.youtube.com/embed/videoId?autoplay=1',
|
playerUri:
|
||||||
|
'https://www.youtube.com/embed/videoId?autoplay=1&playsinline=1',
|
||||||
},
|
},
|
||||||
|
|
||||||
undefined,
|
undefined,
|
||||||
undefined,
|
undefined,
|
||||||
undefined,
|
undefined,
|
||||||
|
|
||||||
{
|
{
|
||||||
type: 'twitch_live',
|
type: 'twitch_video',
|
||||||
channelId: 'channelName',
|
source: 'twitch',
|
||||||
playerUri: `https://player.twitch.tv/?volume=0.5&!muted&autoplay&channel=channelName&parent=localhost`,
|
playerUri: `https://player.twitch.tv/?volume=0.5&!muted&autoplay&channel=channelName&parent=localhost`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 'twitch_live',
|
type: 'twitch_video',
|
||||||
channelId: 'channelName',
|
source: 'twitch',
|
||||||
playerUri: `https://player.twitch.tv/?volume=0.5&!muted&autoplay&channel=channelName&parent=localhost`,
|
playerUri: `https://player.twitch.tv/?volume=0.5&!muted&autoplay&channel=channelName&parent=localhost`,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
type: 'twitch_video',
|
||||||
|
source: 'twitch',
|
||||||
|
playerUri: `https://player.twitch.tv/?volume=0.5&!muted&autoplay&channel=channelName&parent=localhost`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'twitch_video',
|
||||||
|
source: 'twitch',
|
||||||
|
playerUri: `https://clips.twitch.tv/embed?volume=0.5&autoplay=true&clip=clipId&parent=localhost`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'twitch_video',
|
||||||
|
source: 'twitch',
|
||||||
|
playerUri: `https://player.twitch.tv/?volume=0.5&!muted&autoplay&video=videoId&parent=localhost`,
|
||||||
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
type: 'spotify_playlist',
|
type: 'spotify_playlist',
|
||||||
playlistId: 'playlistId',
|
source: 'spotify',
|
||||||
playerUri: `https://open.spotify.com/embed/playlist/playlistId`,
|
playerUri: `https://open.spotify.com/embed/playlist/playlistId`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 'spotify_playlist',
|
type: 'spotify_playlist',
|
||||||
playlistId: 'playlistId',
|
source: 'spotify',
|
||||||
|
playerUri: `https://open.spotify.com/embed/playlist/playlistId`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'spotify_playlist',
|
||||||
|
source: 'spotify',
|
||||||
playerUri: `https://open.spotify.com/embed/playlist/playlistId`,
|
playerUri: `https://open.spotify.com/embed/playlist/playlistId`,
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
type: 'spotify_song',
|
type: 'spotify_song',
|
||||||
songId: 'songId',
|
source: 'spotify',
|
||||||
playerUri: `https://open.spotify.com/embed/track/songId`,
|
playerUri: `https://open.spotify.com/embed/track/songId`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 'spotify_song',
|
type: 'spotify_song',
|
||||||
songId: 'songId',
|
source: 'spotify',
|
||||||
|
playerUri: `https://open.spotify.com/embed/track/songId`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'spotify_song',
|
||||||
|
source: 'spotify',
|
||||||
playerUri: `https://open.spotify.com/embed/track/songId`,
|
playerUri: `https://open.spotify.com/embed/track/songId`,
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
type: 'spotify_album',
|
type: 'spotify_album',
|
||||||
albumId: 'albumId',
|
source: 'spotify',
|
||||||
playerUri: `https://open.spotify.com/embed/album/albumId`,
|
playerUri: `https://open.spotify.com/embed/album/albumId`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 'spotify_album',
|
type: 'spotify_album',
|
||||||
albumId: 'albumId',
|
source: 'spotify',
|
||||||
|
playerUri: `https://open.spotify.com/embed/album/albumId`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'spotify_album',
|
||||||
|
source: 'spotify',
|
||||||
playerUri: `https://open.spotify.com/embed/album/albumId`,
|
playerUri: `https://open.spotify.com/embed/album/albumId`,
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
type: 'soundcloud_track',
|
type: 'soundcloud_track',
|
||||||
user: 'user',
|
source: 'soundcloud',
|
||||||
track: 'track',
|
|
||||||
playerUri: `https://w.soundcloud.com/player/?url=https://soundcloud.com/user/track&auto_play=true&visual=false&hide_related=true`,
|
playerUri: `https://w.soundcloud.com/player/?url=https://soundcloud.com/user/track&auto_play=true&visual=false&hide_related=true`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 'soundcloud_set',
|
type: 'soundcloud_set',
|
||||||
user: 'user',
|
source: 'soundcloud',
|
||||||
set: 'set',
|
|
||||||
playerUri: `https://w.soundcloud.com/player/?url=https://soundcloud.com/user/sets/set&auto_play=true&visual=false&hide_related=true`,
|
playerUri: `https://w.soundcloud.com/player/?url=https://soundcloud.com/user/sets/set&auto_play=true&visual=false&hide_related=true`,
|
||||||
},
|
},
|
||||||
undefined,
|
undefined,
|
||||||
|
|
||||||
|
{
|
||||||
|
type: 'apple_music_playlist',
|
||||||
|
source: 'appleMusic',
|
||||||
|
playerUri:
|
||||||
|
'https://embed.music.apple.com/us/playlist/playlistName/playlistId',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'apple_music_album',
|
||||||
|
source: 'appleMusic',
|
||||||
|
playerUri: 'https://embed.music.apple.com/us/album/albumName/albumId',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'apple_music_song',
|
||||||
|
source: 'appleMusic',
|
||||||
|
playerUri:
|
||||||
|
'https://embed.music.apple.com/us/album/albumName/albumId?i=songId',
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
type: 'vimeo_video',
|
||||||
|
source: 'vimeo',
|
||||||
|
playerUri: 'https://player.vimeo.com/video/videoId?autoplay=1',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'vimeo_video',
|
||||||
|
source: 'vimeo',
|
||||||
|
playerUri: 'https://player.vimeo.com/video/videoId?autoplay=1',
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
type: 'giphy_gif',
|
||||||
|
source: 'giphy',
|
||||||
|
isGif: true,
|
||||||
|
hideDetails: true,
|
||||||
|
metaUri: 'https://giphy.com/gifs/gifId',
|
||||||
|
playerUri: 'https://i.giphy.com/media/gifId/giphy.webp',
|
||||||
|
},
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
|
||||||
|
{
|
||||||
|
type: 'giphy_gif',
|
||||||
|
source: 'giphy',
|
||||||
|
isGif: true,
|
||||||
|
hideDetails: true,
|
||||||
|
metaUri: 'https://giphy.com/gifs/gifId',
|
||||||
|
playerUri: 'https://i.giphy.com/media/gifId/giphy.webp',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'giphy_gif',
|
||||||
|
source: 'giphy',
|
||||||
|
isGif: true,
|
||||||
|
hideDetails: true,
|
||||||
|
metaUri: 'https://giphy.com/gifs/gifId',
|
||||||
|
playerUri: 'https://i.giphy.com/media/gifId/giphy.webp',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'giphy_gif',
|
||||||
|
source: 'giphy',
|
||||||
|
isGif: true,
|
||||||
|
hideDetails: true,
|
||||||
|
metaUri: 'https://giphy.com/gifs/gifId',
|
||||||
|
playerUri: 'https://i.giphy.com/media/gifId/giphy.webp',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'giphy_gif',
|
||||||
|
source: 'giphy',
|
||||||
|
isGif: true,
|
||||||
|
hideDetails: true,
|
||||||
|
metaUri: 'https://giphy.com/gifs/gifId',
|
||||||
|
playerUri: 'https://i.giphy.com/media/gifId/giphy.webp',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'giphy_gif',
|
||||||
|
source: 'giphy',
|
||||||
|
isGif: true,
|
||||||
|
hideDetails: true,
|
||||||
|
metaUri: 'https://giphy.com/gifs/gifId',
|
||||||
|
playerUri: 'https://i.giphy.com/media/gifId/giphy.webp',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'giphy_gif',
|
||||||
|
source: 'giphy',
|
||||||
|
isGif: true,
|
||||||
|
hideDetails: true,
|
||||||
|
metaUri: 'https://giphy.com/gifs/gifId',
|
||||||
|
playerUri: 'https://i.giphy.com/media/gifId/giphy.webp',
|
||||||
|
},
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
|
||||||
|
{
|
||||||
|
type: 'giphy_gif',
|
||||||
|
source: 'giphy',
|
||||||
|
isGif: true,
|
||||||
|
hideDetails: true,
|
||||||
|
metaUri: 'https://giphy.com/gifs/gifId',
|
||||||
|
playerUri: 'https://i.giphy.com/media/gifId/giphy.webp',
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
type: 'giphy_gif',
|
||||||
|
source: 'giphy',
|
||||||
|
isGif: true,
|
||||||
|
hideDetails: true,
|
||||||
|
metaUri: 'https://giphy.com/gifs/gifId',
|
||||||
|
playerUri: 'https://i.giphy.com/media/gifId/giphy.webp',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'giphy_gif',
|
||||||
|
source: 'giphy',
|
||||||
|
isGif: true,
|
||||||
|
hideDetails: true,
|
||||||
|
metaUri: 'https://giphy.com/gifs/gifId',
|
||||||
|
playerUri: 'https://i.giphy.com/media/gifId/giphy.webp',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'giphy_gif',
|
||||||
|
source: 'giphy',
|
||||||
|
isGif: true,
|
||||||
|
hideDetails: true,
|
||||||
|
metaUri: 'https://giphy.com/gifs/gifId',
|
||||||
|
playerUri: 'https://i.giphy.com/media/gifId/giphy.webp',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'giphy_gif',
|
||||||
|
source: 'giphy',
|
||||||
|
isGif: true,
|
||||||
|
hideDetails: true,
|
||||||
|
metaUri: 'https://giphy.com/gifs/gifId',
|
||||||
|
playerUri: 'https://i.giphy.com/media/gifId/giphy.webp',
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
type: 'tenor_gif',
|
||||||
|
source: 'tenor',
|
||||||
|
isGif: true,
|
||||||
|
hideDetails: true,
|
||||||
|
playerUri: 'https://tenor.com/view/gifId.gif',
|
||||||
|
},
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
{
|
||||||
|
type: 'tenor_gif',
|
||||||
|
source: 'tenor',
|
||||||
|
isGif: true,
|
||||||
|
hideDetails: true,
|
||||||
|
playerUri: 'https://tenor.com/view/gifId.gif',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'tenor_gif',
|
||||||
|
source: 'tenor',
|
||||||
|
isGif: true,
|
||||||
|
hideDetails: true,
|
||||||
|
playerUri: 'https://tenor.com/intl/view/gifId.gif',
|
||||||
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
it('correctly grabs the correct id from uri', () => {
|
it('correctly grabs the correct id from uri', () => {
|
||||||
|
|||||||
@@ -1,5 +1,16 @@
|
|||||||
const pkg = require('./package.json')
|
const pkg = require('./package.json')
|
||||||
|
|
||||||
|
const SPLASH_CONFIG = {
|
||||||
|
backgroundColor: '#ffffff',
|
||||||
|
image: './assets/splash.png',
|
||||||
|
resizeMode: 'cover',
|
||||||
|
}
|
||||||
|
const DARK_SPLASH_CONFIG = {
|
||||||
|
backgroundColor: '#001429',
|
||||||
|
image: './assets/splash-dark.png',
|
||||||
|
resizeMode: 'cover',
|
||||||
|
}
|
||||||
|
|
||||||
module.exports = function () {
|
module.exports = function () {
|
||||||
/**
|
/**
|
||||||
* App version number. Should be incremented as part of a release cycle.
|
* App version number. Should be incremented as part of a release cycle.
|
||||||
@@ -9,12 +20,12 @@ module.exports = function () {
|
|||||||
/**
|
/**
|
||||||
* iOS build number. Must be incremented for each TestFlight version.
|
* iOS build number. Must be incremented for each TestFlight version.
|
||||||
*/
|
*/
|
||||||
const IOS_BUILD_NUMBER = '5'
|
const IOS_BUILD_NUMBER = '1'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Android build number. Must be incremented for each release.
|
* Android build number. Must be incremented for each release.
|
||||||
*/
|
*/
|
||||||
const ANDROID_VERSION_CODE = 51
|
const ANDROID_VERSION_CODE = 55
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Uses built-in Expo env vars
|
* Uses built-in Expo env vars
|
||||||
@@ -42,11 +53,7 @@ module.exports = function () {
|
|||||||
orientation: 'portrait',
|
orientation: 'portrait',
|
||||||
icon: './assets/icon.png',
|
icon: './assets/icon.png',
|
||||||
userInterfaceStyle: 'automatic',
|
userInterfaceStyle: 'automatic',
|
||||||
splash: {
|
splash: SPLASH_CONFIG,
|
||||||
image: './assets/splash.png',
|
|
||||||
resizeMode: 'cover',
|
|
||||||
backgroundColor: '#ffffff',
|
|
||||||
},
|
|
||||||
ios: {
|
ios: {
|
||||||
buildNumber: IOS_BUILD_NUMBER,
|
buildNumber: IOS_BUILD_NUMBER,
|
||||||
supportsTablet: false,
|
supportsTablet: false,
|
||||||
@@ -66,6 +73,10 @@ module.exports = function () {
|
|||||||
'Used for profile pictures, posts, and other kinds of content',
|
'Used for profile pictures, posts, and other kinds of content',
|
||||||
},
|
},
|
||||||
associatedDomains: ['applinks:bsky.app', 'applinks:staging.bsky.app'],
|
associatedDomains: ['applinks:bsky.app', 'applinks:staging.bsky.app'],
|
||||||
|
splash: {
|
||||||
|
...SPLASH_CONFIG,
|
||||||
|
dark: DARK_SPLASH_CONFIG,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
androidStatusBar: {
|
androidStatusBar: {
|
||||||
barStyle: 'dark-content',
|
barStyle: 'dark-content',
|
||||||
@@ -95,6 +106,10 @@ module.exports = function () {
|
|||||||
category: ['BROWSABLE', 'DEFAULT'],
|
category: ['BROWSABLE', 'DEFAULT'],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
splash: {
|
||||||
|
...SPLASH_CONFIG,
|
||||||
|
dark: DARK_SPLASH_CONFIG,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
web: {
|
web: {
|
||||||
favicon: './assets/favicon.png',
|
favicon: './assets/favicon.png',
|
||||||
@@ -110,6 +125,9 @@ module.exports = function () {
|
|||||||
[
|
[
|
||||||
'expo-build-properties',
|
'expo-build-properties',
|
||||||
{
|
{
|
||||||
|
ios: {
|
||||||
|
deploymentTarget: '13.4',
|
||||||
|
},
|
||||||
android: {
|
android: {
|
||||||
compileSdkVersion: 34,
|
compileSdkVersion: 34,
|
||||||
targetSdkVersion: 34,
|
targetSdkVersion: 34,
|
||||||
|
|||||||
|
After Width: | Height: | Size: 991 KiB |
@@ -42,6 +42,7 @@ module.exports = function (api) {
|
|||||||
platform: './src/platform',
|
platform: './src/platform',
|
||||||
state: './src/state',
|
state: './src/state',
|
||||||
view: './src/view',
|
view: './src/view',
|
||||||
|
crypto: './src/platform/crypto.ts',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"encoding/xml"
|
"encoding/xml"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
appbsky "github.com/bluesky-social/indigo/api/bsky"
|
appbsky "github.com/bluesky-social/indigo/api/bsky"
|
||||||
@@ -39,11 +40,33 @@ type rss struct {
|
|||||||
|
|
||||||
func (srv *Server) WebProfileRSS(c echo.Context) error {
|
func (srv *Server) WebProfileRSS(c echo.Context) error {
|
||||||
ctx := c.Request().Context()
|
ctx := c.Request().Context()
|
||||||
|
req := c.Request()
|
||||||
|
|
||||||
didParam := c.Param("did")
|
identParam := c.Param("ident")
|
||||||
did, err := syntax.ParseDID(didParam)
|
|
||||||
|
// if not a DID, try parsing as a handle and doing a redirect
|
||||||
|
if !strings.HasPrefix(identParam, "did:") {
|
||||||
|
handle, err := syntax.ParseHandle(identParam)
|
||||||
|
if err != nil {
|
||||||
|
return echo.NewHTTPError(400, fmt.Sprintf("not a valid handle: %s", identParam))
|
||||||
|
}
|
||||||
|
|
||||||
|
// check that public view is Ok, and resolve DID
|
||||||
|
pv, err := appbsky.ActorGetProfile(ctx, srv.xrpcc, handle.String())
|
||||||
|
if err != nil {
|
||||||
|
return echo.NewHTTPError(404, fmt.Sprintf("account not found: %s", handle))
|
||||||
|
}
|
||||||
|
for _, label := range pv.Labels {
|
||||||
|
if label.Src == pv.Did && label.Val == "!no-unauthenticated" {
|
||||||
|
return echo.NewHTTPError(403, fmt.Sprintf("account does not allow public views: %s", handle))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return c.Redirect(http.StatusFound, fmt.Sprintf("/profile/%s/rss", pv.Did))
|
||||||
|
}
|
||||||
|
|
||||||
|
did, err := syntax.ParseDID(identParam)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return echo.NewHTTPError(400, fmt.Sprintf("not a valid DID: %s", didParam))
|
return echo.NewHTTPError(400, fmt.Sprintf("not a valid DID: %s", identParam))
|
||||||
}
|
}
|
||||||
|
|
||||||
// check that public view is Ok
|
// check that public view is Ok
|
||||||
@@ -84,7 +107,7 @@ func (srv *Server) WebProfileRSS(c echo.Context) error {
|
|||||||
pubDate = createdAt.Time().Format(time.RFC822Z)
|
pubDate = createdAt.Time().Format(time.RFC822Z)
|
||||||
}
|
}
|
||||||
posts = append(posts, Item{
|
posts = append(posts, Item{
|
||||||
Link: fmt.Sprintf("https://bsky.app/profile/%s/post/%s", pv.Handle, aturi.RecordKey().String()),
|
Link: fmt.Sprintf("https://%s/profile/%s/post/%s", req.Host, pv.Handle, aturi.RecordKey().String()),
|
||||||
Description: rec.Text,
|
Description: rec.Text,
|
||||||
PubDate: pubDate,
|
PubDate: pubDate,
|
||||||
GUID: ItemGUID{
|
GUID: ItemGUID{
|
||||||
@@ -105,7 +128,7 @@ func (srv *Server) WebProfileRSS(c echo.Context) error {
|
|||||||
feed := &rss{
|
feed := &rss{
|
||||||
Version: "2.0",
|
Version: "2.0",
|
||||||
Description: desc,
|
Description: desc,
|
||||||
Link: fmt.Sprintf("https://bsky.app/profile/%s", pv.Handle),
|
Link: fmt.Sprintf("https://%s/profile/%s", req.Host, pv.Handle),
|
||||||
Title: title,
|
Title: title,
|
||||||
Item: posts,
|
Item: posts,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -193,6 +193,7 @@ func serve(cctx *cli.Context) error {
|
|||||||
e.GET("/settings/home-feed", server.WebGeneric)
|
e.GET("/settings/home-feed", server.WebGeneric)
|
||||||
e.GET("/settings/saved-feeds", server.WebGeneric)
|
e.GET("/settings/saved-feeds", server.WebGeneric)
|
||||||
e.GET("/settings/threads", server.WebGeneric)
|
e.GET("/settings/threads", server.WebGeneric)
|
||||||
|
e.GET("/settings/external-embeds", server.WebGeneric)
|
||||||
e.GET("/sys/debug", server.WebGeneric)
|
e.GET("/sys/debug", server.WebGeneric)
|
||||||
e.GET("/sys/log", server.WebGeneric)
|
e.GET("/sys/log", server.WebGeneric)
|
||||||
e.GET("/support", server.WebGeneric)
|
e.GET("/support", server.WebGeneric)
|
||||||
@@ -210,7 +211,7 @@ func serve(cctx *cli.Context) error {
|
|||||||
e.GET("/profile/:handle/feed/:rkey/liked-by", server.WebGeneric)
|
e.GET("/profile/:handle/feed/:rkey/liked-by", server.WebGeneric)
|
||||||
|
|
||||||
// profile RSS feed (DID not handle)
|
// profile RSS feed (DID not handle)
|
||||||
e.GET("/profile/:did/rss", server.WebProfileRSS)
|
e.GET("/profile/:ident/rss", server.WebProfileRSS)
|
||||||
|
|
||||||
// post endpoints; only first populates info
|
// post endpoints; only first populates info
|
||||||
e.GET("/profile/:handle/post/:rkey", server.WebPost)
|
e.GET("/profile/:handle/post/:rkey", server.WebPost)
|
||||||
@@ -336,7 +337,11 @@ func (srv *Server) WebPost(c echo.Context) error {
|
|||||||
data["postView"] = postView
|
data["postView"] = postView
|
||||||
data["requestURI"] = fmt.Sprintf("https://%s%s", req.Host, req.URL.Path)
|
data["requestURI"] = fmt.Sprintf("https://%s%s", req.Host, req.URL.Path)
|
||||||
if postView.Embed != nil && postView.Embed.EmbedImages_View != nil {
|
if postView.Embed != nil && postView.Embed.EmbedImages_View != nil {
|
||||||
data["imgThumbUrl"] = postView.Embed.EmbedImages_View.Images[0].Thumb
|
var thumbUrls []string
|
||||||
|
for i := range postView.Embed.EmbedImages_View.Images {
|
||||||
|
thumbUrls = append(thumbUrls, postView.Embed.EmbedImages_View.Images[i].Thumb)
|
||||||
|
}
|
||||||
|
data["imgThumbUrls"] = thumbUrls
|
||||||
}
|
}
|
||||||
return c.Render(http.StatusOK, "post.html", data)
|
return c.Render(http.StatusOK, "post.html", data)
|
||||||
}
|
}
|
||||||
@@ -370,6 +375,7 @@ func (srv *Server) WebProfile(c echo.Context) error {
|
|||||||
req := c.Request()
|
req := c.Request()
|
||||||
data["profileView"] = pv
|
data["profileView"] = pv
|
||||||
data["requestURI"] = fmt.Sprintf("https://%s%s", req.Host, req.URL.Path)
|
data["requestURI"] = fmt.Sprintf("https://%s%s", req.Host, req.URL.Path)
|
||||||
|
data["requestHost"] = req.Host
|
||||||
return c.Render(http.StatusOK, "profile.html", data)
|
return c.Render(http.StatusOK, "profile.html", data)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 7.6 KiB |
|
Before Width: | Height: | Size: 316 B After Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 769 B After Width: | Height: | Size: 2.2 KiB |
|
Before Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 6.3 KiB After Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 16 KiB |
@@ -48,6 +48,7 @@
|
|||||||
--text: white;
|
--text: white;
|
||||||
--background: black;
|
--background: black;
|
||||||
--backgroundLight: #26272D;
|
--backgroundLight: #26272D;
|
||||||
|
color-scheme: dark;
|
||||||
}
|
}
|
||||||
@media (prefers-color-scheme: light) {
|
@media (prefers-color-scheme: light) {
|
||||||
html.colorMode--system {
|
html.colorMode--system {
|
||||||
@@ -61,6 +62,7 @@
|
|||||||
--text: white;
|
--text: white;
|
||||||
--background: black;
|
--background: black;
|
||||||
--backgroundLight: #26272D;
|
--backgroundLight: #26272D;
|
||||||
|
color-scheme: dark;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -25,8 +25,10 @@
|
|||||||
<meta name="description" content="{{ postView.Record.Val.Text }}">
|
<meta name="description" content="{{ postView.Record.Val.Text }}">
|
||||||
<meta property="og:description" content="{{ postView.Record.Val.Text }}">
|
<meta property="og:description" content="{{ postView.Record.Val.Text }}">
|
||||||
{% endif -%}
|
{% endif -%}
|
||||||
{%- if imgThumbUrl %}
|
{%- if imgThumbUrls %}
|
||||||
|
{% for imgThumbUrl in imgThumbUrls %}
|
||||||
<meta property="og:image" content="{{ imgThumbUrl }}">
|
<meta property="og:image" content="{{ imgThumbUrl }}">
|
||||||
|
{% endfor %}
|
||||||
<meta name="twitter:card" content="summary_large_image">
|
<meta name="twitter:card" content="summary_large_image">
|
||||||
{%- elif postView.Author.Avatar %}
|
{%- elif postView.Author.Avatar %}
|
||||||
{# Don't use avatar image in cards; usually looks bad #}
|
{# Don't use avatar image in cards; usually looks bad #}
|
||||||
|
|||||||
@@ -34,7 +34,9 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
<meta name="twitter:label1" content="Account DID">
|
<meta name="twitter:label1" content="Account DID">
|
||||||
<meta name="twitter:value1" content="{{ profileView.Did }}">
|
<meta name="twitter:value1" content="{{ profileView.Did }}">
|
||||||
<link rel="alternate" type="application/rss+xml" href="/profile/{{ profileView.Did }}/rss">
|
{%- if requestHost %}
|
||||||
|
<link rel="alternate" type="application/rss+xml" href="https://{{ requestHost }}/profile/{{ profileView.Did }}/rss">
|
||||||
|
{% endif %}
|
||||||
{% endif -%}
|
{% endif -%}
|
||||||
{%- endblock %}
|
{%- endblock %}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
/** @type {import('@lingui/conf').LinguiConfig} */
|
/** @type {import('@lingui/conf').LinguiConfig} */
|
||||||
module.exports = {
|
module.exports = {
|
||||||
locales: ['en', 'hi', 'ja', 'fr', 'de', 'es'],
|
locales: ['en', 'de', 'es', 'fr', 'hi', 'id', 'ja', 'ko', 'pt-BR', 'uk'],
|
||||||
catalogs: [
|
catalogs: [
|
||||||
{
|
{
|
||||||
path: '<rootDir>/src/locale/locales/{locale}/messages',
|
path: '<rootDir>/src/locale/locales/{locale}/messages',
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
{
|
{
|
||||||
"name": "bsky.app",
|
"name": "bsky.app",
|
||||||
"version": "1.60.0",
|
"version": "1.63.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"prepare": "is-ci || husky install",
|
"prepare": "is-ci || husky install",
|
||||||
"postinstall": "patch-package && yarn intl:compile",
|
"postinstall": "patch-package && yarn intl:compile",
|
||||||
@@ -20,7 +23,7 @@
|
|||||||
"test-coverage": "NODE_ENV=test jest --coverage",
|
"test-coverage": "NODE_ENV=test jest --coverage",
|
||||||
"lint": "eslint ./src --ext .js,.jsx,.ts,.tsx",
|
"lint": "eslint ./src --ext .js,.jsx,.ts,.tsx",
|
||||||
"typecheck": "tsc --project ./tsconfig.check.json",
|
"typecheck": "tsc --project ./tsconfig.check.json",
|
||||||
"e2e:mock-server": "./jest/dev-infra/with-test-redis-and-db.sh ts-node __e2e__/mock-server.ts",
|
"e2e:mock-server": "./jest/dev-infra/with-test-redis-and-db.sh ts-node --project tsconfig.e2e.json __e2e__/mock-server.ts",
|
||||||
"e2e:metro": "NODE_ENV=test RN_SRC_EXT=e2e.ts,e2e.tsx expo run:ios",
|
"e2e:metro": "NODE_ENV=test RN_SRC_EXT=e2e.ts,e2e.tsx expo run:ios",
|
||||||
"e2e:build": "NODE_ENV=test detox build -c ios.sim.debug",
|
"e2e:build": "NODE_ENV=test detox build -c ios.sim.debug",
|
||||||
"e2e:run": "NODE_ENV=test detox test --configuration ios.sim.debug --take-screenshots all",
|
"e2e:run": "NODE_ENV=test detox test --configuration ios.sim.debug --take-screenshots all",
|
||||||
@@ -32,10 +35,11 @@
|
|||||||
"intl:build": "yarn intl:check && yarn intl:compile",
|
"intl:build": "yarn intl:check && yarn intl:compile",
|
||||||
"intl:check": "yarn intl:extract && git diff-index -G'(^[^\\*# /])|(^#\\w)|(^\\s+[^\\*#/])' HEAD || (echo '\n⚠️ i18n detected un-extracted translations\n' && exit 1)",
|
"intl:check": "yarn intl:extract && git diff-index -G'(^[^\\*# /])|(^#\\w)|(^\\s+[^\\*#/])' HEAD || (echo '\n⚠️ i18n detected un-extracted translations\n' && exit 1)",
|
||||||
"intl:extract": "lingui extract",
|
"intl:extract": "lingui extract",
|
||||||
"intl:compile": "lingui compile"
|
"intl:compile": "lingui compile",
|
||||||
|
"nuke": "rm -rf ./node_modules && rm -rf ./ios && rm -rf ./android"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@atproto/api": "^0.7.4",
|
"@atproto/api": "^0.8.0",
|
||||||
"@bam.tech/react-native-image-resizer": "^3.0.4",
|
"@bam.tech/react-native-image-resizer": "^3.0.4",
|
||||||
"@braintree/sanitize-url": "^6.0.2",
|
"@braintree/sanitize-url": "^6.0.2",
|
||||||
"@emoji-mart/react": "^1.1.1",
|
"@emoji-mart/react": "^1.1.1",
|
||||||
@@ -49,14 +53,14 @@
|
|||||||
"@lingui/react": "^4.5.0",
|
"@lingui/react": "^4.5.0",
|
||||||
"@mattermost/react-native-paste-input": "^0.6.4",
|
"@mattermost/react-native-paste-input": "^0.6.4",
|
||||||
"@miblanchard/react-native-slider": "^2.3.1",
|
"@miblanchard/react-native-slider": "^2.3.1",
|
||||||
"@react-native-async-storage/async-storage": "1.18.2",
|
"@react-native-async-storage/async-storage": "1.21.0",
|
||||||
"@react-native-camera-roll/camera-roll": "^5.2.2",
|
"@react-native-camera-roll/camera-roll": "^5.2.2",
|
||||||
"@react-native-clipboard/clipboard": "^1.10.0",
|
"@react-native-clipboard/clipboard": "^1.10.0",
|
||||||
"@react-native-community/blur": "^4.3.0",
|
"@react-native-community/blur": "^4.3.0",
|
||||||
"@react-native-community/datetimepicker": "7.2.0",
|
"@react-native-community/datetimepicker": "7.6.1",
|
||||||
"@react-native-masked-view/masked-view": "^0.3.1",
|
"@react-native-masked-view/masked-view": "0.3.0",
|
||||||
"@react-native-menu/menu": "^0.8.0",
|
"@react-native-menu/menu": "^0.8.0",
|
||||||
"@react-native-picker/picker": "2.4.10",
|
"@react-native-picker/picker": "2.5.1",
|
||||||
"@react-navigation/bottom-tabs": "^6.5.7",
|
"@react-navigation/bottom-tabs": "^6.5.7",
|
||||||
"@react-navigation/drawer": "^6.6.2",
|
"@react-navigation/drawer": "^6.6.2",
|
||||||
"@react-navigation/native": "^6.1.6",
|
"@react-navigation/native": "^6.1.6",
|
||||||
@@ -65,7 +69,7 @@
|
|||||||
"@segment/analytics-react": "^1.0.0-rc1",
|
"@segment/analytics-react": "^1.0.0-rc1",
|
||||||
"@segment/analytics-react-native": "^2.10.1",
|
"@segment/analytics-react-native": "^2.10.1",
|
||||||
"@segment/sovran-react-native": "^0.4.5",
|
"@segment/sovran-react-native": "^0.4.5",
|
||||||
"@sentry/react-native": "5.10.0",
|
"@sentry/react-native": "5.5.0",
|
||||||
"@tanstack/react-query": "^5.8.1",
|
"@tanstack/react-query": "^5.8.1",
|
||||||
"@tiptap/core": "^2.0.0-beta.220",
|
"@tiptap/core": "^2.0.0-beta.220",
|
||||||
"@tiptap/extension-document": "^2.0.0-beta.220",
|
"@tiptap/extension-document": "^2.0.0-beta.220",
|
||||||
@@ -90,24 +94,25 @@
|
|||||||
"email-validator": "^2.0.4",
|
"email-validator": "^2.0.4",
|
||||||
"emoji-mart": "^5.5.2",
|
"emoji-mart": "^5.5.2",
|
||||||
"eventemitter3": "^5.0.1",
|
"eventemitter3": "^5.0.1",
|
||||||
"expo": "^49.0.8",
|
"expo": "^50.0.0-preview.7",
|
||||||
"expo-application": "~5.3.0",
|
"expo-application": "~5.8.1",
|
||||||
"expo-build-properties": "~0.8.3",
|
"expo-build-properties": "^0.11.0",
|
||||||
"expo-camera": "13.5.1",
|
"expo-camera": "~14.0.1",
|
||||||
"expo-constants": "~14.4.2",
|
"expo-constants": "~15.4.2",
|
||||||
"expo-dev-client": "2.4.7",
|
"expo-dev-client": "~3.3.4",
|
||||||
"expo-device": "~5.4.0",
|
"expo-device": "~5.9.1",
|
||||||
"expo-image": "~1.3.2",
|
"expo-image": "~1.10.1",
|
||||||
"expo-image-manipulator": "~11.5.0",
|
"expo-image-manipulator": "^11.8.0",
|
||||||
"expo-image-picker": "~14.5.0",
|
"expo-image-picker": "~14.7.1",
|
||||||
"expo-localization": "~14.3.0",
|
"expo-localization": "~14.8.1",
|
||||||
"expo-media-library": "~15.4.1",
|
"expo-media-library": "~15.9.1",
|
||||||
"expo-notifications": "~0.20.1",
|
"expo-notifications": "~0.27.2",
|
||||||
"expo-sharing": "~11.5.0",
|
"expo-sharing": "^11.10.0",
|
||||||
"expo-splash-screen": "~0.20.5",
|
"expo-splash-screen": "~0.26.1",
|
||||||
"expo-status-bar": "~1.6.0",
|
"expo-status-bar": "~1.11.1",
|
||||||
"expo-system-ui": "~2.4.0",
|
"expo-system-ui": "~2.9.2",
|
||||||
"expo-updates": "~0.18.12",
|
"expo-task-manager": "~11.7.0",
|
||||||
|
"expo-updates": "~0.24.5",
|
||||||
"fast-text-encoding": "^1.0.6",
|
"fast-text-encoding": "^1.0.6",
|
||||||
"history": "^5.3.0",
|
"history": "^5.3.0",
|
||||||
"js-sha256": "^0.9.0",
|
"js-sha256": "^0.9.0",
|
||||||
@@ -135,34 +140,32 @@
|
|||||||
"react-avatar-editor": "^13.0.0",
|
"react-avatar-editor": "^13.0.0",
|
||||||
"react-circular-progressbar": "^2.1.0",
|
"react-circular-progressbar": "^2.1.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-native": "0.72.5",
|
"react-native": "0.73.1",
|
||||||
"react-native-appstate-hook": "^1.0.6",
|
"react-native-appstate-hook": "^1.0.6",
|
||||||
"react-native-drawer-layout": "^4.0.0-alpha.3",
|
"react-native-drawer-layout": "^4.0.0-alpha.3",
|
||||||
"react-native-fs": "^2.20.0",
|
"react-native-fs": "^2.20.0",
|
||||||
"react-native-gesture-handler": "^2.12.1",
|
"react-native-gesture-handler": "~2.14.0",
|
||||||
"react-native-get-random-values": "^1.8.0",
|
"react-native-get-random-values": "~1.8.0",
|
||||||
"react-native-haptic-feedback": "^1.14.0",
|
"react-native-haptic-feedback": "^1.14.0",
|
||||||
"react-native-image-crop-picker": "^0.38.1",
|
"react-native-image-crop-picker": "^0.38.1",
|
||||||
"react-native-inappbrowser-reborn": "^3.6.3",
|
"react-native-inappbrowser-reborn": "^3.6.3",
|
||||||
"react-native-ios-context-menu": "^1.15.3",
|
"react-native-ios-context-menu": "^1.15.3",
|
||||||
"react-native-linear-gradient": "^2.6.2",
|
"react-native-linear-gradient": "^2.6.2",
|
||||||
"react-native-pager-view": "6.1.4",
|
"react-native-pager-view": "6.2.2",
|
||||||
"react-native-picker-select": "^8.1.0",
|
"react-native-picker-select": "^8.1.0",
|
||||||
"react-native-progress": "bluesky-social/react-native-progress",
|
"react-native-progress": "bluesky-social/react-native-progress",
|
||||||
"react-native-reanimated": "^3.6.0",
|
"react-native-reanimated": "^3.6.0",
|
||||||
"react-native-root-siblings": "^4.1.1",
|
"react-native-root-siblings": "^4.1.1",
|
||||||
"react-native-safe-area-context": "4.6.3",
|
"react-native-safe-area-context": "4.7.4",
|
||||||
"react-native-screens": "~3.22.0",
|
"react-native-screens": "~3.27.0",
|
||||||
"react-native-splash-screen": "^3.3.0",
|
"react-native-svg": "14.0.0",
|
||||||
"react-native-svg": "13.9.0",
|
|
||||||
"react-native-url-polyfill": "^1.3.0",
|
"react-native-url-polyfill": "^1.3.0",
|
||||||
"react-native-uuid": "^2.0.1",
|
"react-native-uuid": "^2.0.1",
|
||||||
"react-native-version-number": "^0.3.6",
|
"react-native-version-number": "^0.3.6",
|
||||||
"react-native-web": "~0.19.6",
|
"react-native-web": "~0.19.6",
|
||||||
"react-native-web-linear-gradient": "^1.1.2",
|
"react-native-web-linear-gradient": "^1.1.2",
|
||||||
"react-native-web-webview": "^1.0.2",
|
"react-native-web-webview": "^1.0.2",
|
||||||
"react-native-webview": "^13.6.2",
|
"react-native-webview": "^13.6.3",
|
||||||
"react-native-youtube-iframe": "^2.3.0",
|
|
||||||
"react-responsive": "^9.0.2",
|
"react-responsive": "^9.0.2",
|
||||||
"rn-fetch-blob": "^0.12.0",
|
"rn-fetch-blob": "^0.12.0",
|
||||||
"sentry-expo": "~7.0.1",
|
"sentry-expo": "~7.0.1",
|
||||||
@@ -178,10 +181,13 @@
|
|||||||
"@babel/preset-env": "^7.20.0",
|
"@babel/preset-env": "^7.20.0",
|
||||||
"@babel/runtime": "^7.20.0",
|
"@babel/runtime": "^7.20.0",
|
||||||
"@did-plc/server": "^0.0.1",
|
"@did-plc/server": "^0.0.1",
|
||||||
|
"@expo/config-plugins": "7.8.0",
|
||||||
|
"@expo/prebuild-config": "6.7.0",
|
||||||
"@lingui/cli": "^4.5.0",
|
"@lingui/cli": "^4.5.0",
|
||||||
"@lingui/macro": "^4.5.0",
|
"@lingui/macro": "^4.5.0",
|
||||||
"@pmmmwh/react-refresh-webpack-plugin": "^0.5.11",
|
"@pmmmwh/react-refresh-webpack-plugin": "^0.5.11",
|
||||||
"@react-native-community/eslint-config": "^3.0.0",
|
"@react-native-community/eslint-config": "^3.0.0",
|
||||||
|
"@react-native/typescript-config": "^0.74.0",
|
||||||
"@testing-library/jest-native": "^5.4.1",
|
"@testing-library/jest-native": "^5.4.1",
|
||||||
"@testing-library/react-native": "^11.5.2",
|
"@testing-library/react-native": "^11.5.2",
|
||||||
"@tsconfig/react-native": "^2.0.3",
|
"@tsconfig/react-native": "^2.0.3",
|
||||||
@@ -203,12 +209,13 @@
|
|||||||
"@types/react-test-renderer": "^17.0.1",
|
"@types/react-test-renderer": "^17.0.1",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.48.2",
|
"@typescript-eslint/eslint-plugin": "^5.48.2",
|
||||||
"@typescript-eslint/parser": "^5.48.2",
|
"@typescript-eslint/parser": "^5.48.2",
|
||||||
"babel-jest": "^29.4.2",
|
"babel-jest": "^29.7.0",
|
||||||
"babel-loader": "^9.1.2",
|
"babel-loader": "^9.1.2",
|
||||||
"babel-plugin-macros": "^3.1.0",
|
"babel-plugin-macros": "^3.1.0",
|
||||||
"babel-plugin-module-resolver": "^5.0.0",
|
"babel-plugin-module-resolver": "^5.0.0",
|
||||||
"babel-plugin-react-native-web": "^0.18.12",
|
"babel-plugin-react-native-web": "^0.18.12",
|
||||||
"detox": "^20.13.0",
|
"babel-preset-expo": "^10.0.0",
|
||||||
|
"detox": "^20.14.8",
|
||||||
"eslint": "^8.19.0",
|
"eslint": "^8.19.0",
|
||||||
"eslint-plugin-detox": "^1.0.0",
|
"eslint-plugin-detox": "^1.0.0",
|
||||||
"eslint-plugin-ft-flow": "^2.0.3",
|
"eslint-plugin-ft-flow": "^2.0.3",
|
||||||
@@ -218,8 +225,8 @@
|
|||||||
"html-webpack-plugin": "^5.5.0",
|
"html-webpack-plugin": "^5.5.0",
|
||||||
"husky": "^8.0.3",
|
"husky": "^8.0.3",
|
||||||
"is-ci": "^3.0.1",
|
"is-ci": "^3.0.1",
|
||||||
"jest": "^29.4.3",
|
"jest": "^29.7.0",
|
||||||
"jest-expo": "^49.0.0",
|
"jest-expo": "^50.0.1",
|
||||||
"jest-junit": "^15.0.0",
|
"jest-junit": "^15.0.0",
|
||||||
"lint-staged": "^13.2.3",
|
"lint-staged": "^13.2.3",
|
||||||
"metro-react-native-babel-preset": "^0.73.7",
|
"metro-react-native-babel-preset": "^0.73.7",
|
||||||
@@ -229,7 +236,7 @@
|
|||||||
"react-scripts": "^5.0.1",
|
"react-scripts": "^5.0.1",
|
||||||
"react-test-renderer": "18.2.0",
|
"react-test-renderer": "18.2.0",
|
||||||
"ts-node": "^10.9.1",
|
"ts-node": "^10.9.1",
|
||||||
"typescript": "^5.1.3",
|
"typescript": "^5.3.3",
|
||||||
"url-loader": "^4.1.1",
|
"url-loader": "^4.1.1",
|
||||||
"webpack": "^5.75.0",
|
"webpack": "^5.75.0",
|
||||||
"webpack-cli": "^5.0.1",
|
"webpack-cli": "^5.0.1",
|
||||||
|
|||||||
@@ -1,14 +0,0 @@
|
|||||||
diff --git a/node_modules/babel-preset-expo/index.js b/node_modules/babel-preset-expo/index.js
|
|
||||||
index 2099ee3..2b9e092 100644
|
|
||||||
--- a/node_modules/babel-preset-expo/index.js
|
|
||||||
+++ b/node_modules/babel-preset-expo/index.js
|
|
||||||
@@ -105,7 +105,8 @@ module.exports = function (api, options = {}) {
|
|
||||||
],
|
|
||||||
],
|
|
||||||
plugins: [
|
|
||||||
- getObjectRestSpreadPlugin(),
|
|
||||||
+ // - dan: This will be disabled anyway when we upgrade Expo, but let's do it now.
|
|
||||||
+ // getObjectRestSpreadPlugin(),
|
|
||||||
...extraPlugins,
|
|
||||||
getAliasPlugin(),
|
|
||||||
[require.resolve('@babel/plugin-proposal-decorators'), { legacy: true }],
|
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
diff --git a/node_modules/expo-image-picker/android/src/main/java/expo/modules/imagepicker/contracts/ImageLibraryContract.kt b/node_modules/expo-image-picker/android/src/main/java/expo/modules/imagepicker/contracts/ImageLibraryContract.kt
|
||||||
|
index ff15c91..41aaf12 100644
|
||||||
|
--- a/node_modules/expo-image-picker/android/src/main/java/expo/modules/imagepicker/contracts/ImageLibraryContract.kt
|
||||||
|
+++ b/node_modules/expo-image-picker/android/src/main/java/expo/modules/imagepicker/contracts/ImageLibraryContract.kt
|
||||||
|
@@ -26,51 +26,26 @@ import java.io.Serializable
|
||||||
|
* @see [androidx.activity.result.contract.ActivityResultContracts.GetMultipleContents]
|
||||||
|
*/
|
||||||
|
internal class ImageLibraryContract(
|
||||||
|
- private val appContextProvider: AppContextProvider
|
||||||
|
+ private val appContextProvider: AppContextProvider,
|
||||||
|
) : AppContextActivityResultContract<ImageLibraryContractOptions, ImagePickerContractResult> {
|
||||||
|
private val contentResolver: ContentResolver
|
||||||
|
get() = appContextProvider.appContext.reactContext?.contentResolver
|
||||||
|
?: throw Exceptions.ReactContextLost()
|
||||||
|
|
||||||
|
override fun createIntent(context: Context, input: ImageLibraryContractOptions): Intent {
|
||||||
|
- val request = PickVisualMediaRequest.Builder()
|
||||||
|
- .setMediaType(
|
||||||
|
- when (input.options.mediaTypes) {
|
||||||
|
- MediaTypes.VIDEOS -> {
|
||||||
|
- PickVisualMedia.VideoOnly
|
||||||
|
- }
|
||||||
|
-
|
||||||
|
- MediaTypes.IMAGES -> {
|
||||||
|
- PickVisualMedia.ImageOnly
|
||||||
|
- }
|
||||||
|
-
|
||||||
|
- else -> {
|
||||||
|
- PickVisualMedia.ImageAndVideo
|
||||||
|
- }
|
||||||
|
- }
|
||||||
|
- )
|
||||||
|
- .build()
|
||||||
|
+ val intent = Intent(Intent.ACTION_GET_CONTENT)
|
||||||
|
+ .addCategory(Intent.CATEGORY_OPENABLE)
|
||||||
|
+ .setType("image/*")
|
||||||
|
|
||||||
|
if (input.options.allowsMultipleSelection) {
|
||||||
|
- val selectionLimit = input.options.selectionLimit
|
||||||
|
-
|
||||||
|
- if (selectionLimit == 1) {
|
||||||
|
- // If multiple selection is allowed but the limit is 1, we should ignore
|
||||||
|
- // the multiple selection flag and just treat it as a single selection.
|
||||||
|
- return PickVisualMedia().createIntent(context, request)
|
||||||
|
+ if(input.options.selectionLimit == 1) {
|
||||||
|
+ return intent
|
||||||
|
}
|
||||||
|
|
||||||
|
- if (selectionLimit > 1) {
|
||||||
|
- return PickMultipleVisualMedia(selectionLimit).createIntent(context, request)
|
||||||
|
- }
|
||||||
|
-
|
||||||
|
- // If the selection limit is 0, it is the same as unlimited selection.
|
||||||
|
- if (selectionLimit == UNLIMITED_SELECTION) {
|
||||||
|
- return PickMultipleVisualMedia().createIntent(context, request)
|
||||||
|
- }
|
||||||
|
+ intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
- return PickVisualMedia().createIntent(context, request)
|
||||||
|
+ return intent
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun parseResult(input: ImageLibraryContractOptions, resultCode: Int, intent: Intent?) =
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
added by https://github.com/bluesky-social/social-app/pull/2384#pullrequestreview-1800985521
|
||||||
|
|
||||||
|
hackfixes the image picker on android so that the user can select from their typical image sources
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
diff --git a/node_modules/metro-transform-worker/src/index.js b/node_modules/metro-transform-worker/src/index.js
|
diff --git a/node_modules/metro-transform-worker/src/index.js b/node_modules/metro-transform-worker/src/index.js
|
||||||
index 27d4cb3..fd71f47 100644
|
index cae11e7..42f251b 100644
|
||||||
--- a/node_modules/metro-transform-worker/src/index.js
|
--- a/node_modules/metro-transform-worker/src/index.js
|
||||||
+++ b/node_modules/metro-transform-worker/src/index.js
|
+++ b/node_modules/metro-transform-worker/src/index.js
|
||||||
@@ -190,6 +190,10 @@ async function transformJS(file, { config, options, projectRoot }) {
|
@@ -189,6 +189,10 @@ async function transformJS(file, { config, options, projectRoot }) {
|
||||||
let dependencyMapName = "";
|
let dependencyMapName = "";
|
||||||
let dependencies;
|
let dependencies;
|
||||||
let wrappedAst;
|
let wrappedAst;
|
||||||
@@ -13,7 +13,7 @@ index 27d4cb3..fd71f47 100644
|
|||||||
|
|
||||||
// If the module to transform is a script (meaning that is not part of the
|
// If the module to transform is a script (meaning that is not part of the
|
||||||
// dependency graph and it code will just be prepended to the bundle modules),
|
// dependency graph and it code will just be prepended to the bundle modules),
|
||||||
@@ -229,19 +233,20 @@ async function transformJS(file, { config, options, projectRoot }) {
|
@@ -228,19 +232,20 @@ async function transformJS(file, { config, options, projectRoot }) {
|
||||||
if (config.unstable_disableModuleWrapping === true) {
|
if (config.unstable_disableModuleWrapping === true) {
|
||||||
wrappedAst = ast;
|
wrappedAst = ast;
|
||||||
} else {
|
} else {
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
diff --git a/node_modules/react-native/Libraries/Text/TextInput/RCTBackedTextInputDelegateAdapter.m b/node_modules/react-native/Libraries/Text/TextInput/RCTBackedTextInputDelegateAdapter.m
|
diff --git a/node_modules/react-native/Libraries/Text/TextInput/RCTBackedTextInputDelegateAdapter.mm b/node_modules/react-native/Libraries/Text/TextInput/RCTBackedTextInputDelegateAdapter.mm
|
||||||
index 9dca6a5..090bda5 100644
|
index 9dca6a5..090bda5 100644
|
||||||
--- a/node_modules/react-native/Libraries/Text/TextInput/RCTBackedTextInputDelegateAdapter.m
|
--- a/node_modules/react-native/Libraries/Text/TextInput/RCTBackedTextInputDelegateAdapter.mm
|
||||||
+++ b/node_modules/react-native/Libraries/Text/TextInput/RCTBackedTextInputDelegateAdapter.m
|
+++ b/node_modules/react-native/Libraries/Text/TextInput/RCTBackedTextInputDelegateAdapter.mm
|
||||||
@@ -266,11 +266,10 @@ static void *TextFieldSelectionObservingContext = &TextFieldSelectionObservingCo
|
@@ -266,11 +266,10 @@ static void *TextFieldSelectionObservingContext = &TextFieldSelectionObservingCo
|
||||||
|
|
||||||
- (void)textViewDidChangeSelection:(__unused UITextView *)textView
|
- (void)textViewDidChangeSelection:(__unused UITextView *)textView
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
diff --git a/node_modules/react-native-pager-view/ios/ReactNativePageView.m b/node_modules/react-native-pager-view/ios/ReactNativePageView.m
|
|
||||||
index ab0fc7f..1ace752 100644
|
|
||||||
--- a/node_modules/react-native-pager-view/ios/ReactNativePageView.m
|
|
||||||
+++ b/node_modules/react-native-pager-view/ios/ReactNativePageView.m
|
|
||||||
@@ -1,6 +1,6 @@
|
|
||||||
|
|
||||||
#import "ReactNativePageView.h"
|
|
||||||
-#import "React/RCTLog.h"
|
|
||||||
+#import <React/RCTLog.h>
|
|
||||||
#import <React/RCTViewManager.h>
|
|
||||||
|
|
||||||
#import "UIViewController+CreateExtension.h"
|
|
||||||
@@ -9,7 +9,7 @@
|
|
||||||
#import "RCTOnPageSelected.h"
|
|
||||||
#import <math.h>
|
|
||||||
|
|
||||||
-@interface ReactNativePageView () <UIPageViewControllerDataSource, UIPageViewControllerDelegate, UIScrollViewDelegate>
|
|
||||||
+@interface ReactNativePageView () <UIPageViewControllerDataSource, UIPageViewControllerDelegate, UIScrollViewDelegate, UIGestureRecognizerDelegate>
|
|
||||||
|
|
||||||
@property(nonatomic, strong) UIPageViewController *reactPageViewController;
|
|
||||||
@property(nonatomic, strong) RCTEventDispatcher *eventDispatcher;
|
|
||||||
@@ -80,6 +80,10 @@ - (void)didMoveToWindow {
|
|
||||||
[self setupInitialController];
|
|
||||||
}
|
|
||||||
|
|
||||||
+ UIPanGestureRecognizer* panGestureRecognizer = [UIPanGestureRecognizer new];
|
|
||||||
+ panGestureRecognizer.delegate = self;
|
|
||||||
+ [self addGestureRecognizer: panGestureRecognizer];
|
|
||||||
+
|
|
||||||
if (self.reactViewController.navigationController != nil && self.reactViewController.navigationController.interactivePopGestureRecognizer != nil) {
|
|
||||||
[self.scrollView.panGestureRecognizer requireGestureRecognizerToFail:self.reactViewController.navigationController.interactivePopGestureRecognizer];
|
|
||||||
}
|
|
||||||
@@ -463,4 +467,21 @@ - (NSString *)determineScrollDirection:(UIScrollView *)scrollView {
|
|
||||||
- (BOOL)isLtrLayout {
|
|
||||||
return [_layoutDirection isEqualToString:@"ltr"];
|
|
||||||
}
|
|
||||||
+
|
|
||||||
+- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer {
|
|
||||||
+ if (!_overdrag && otherGestureRecognizer == self.scrollView.panGestureRecognizer) {
|
|
||||||
+ UIPanGestureRecognizer* p = (UIPanGestureRecognizer*) gestureRecognizer;
|
|
||||||
+ CGPoint velocity = [p velocityInView:self];
|
|
||||||
+ if (self.currentIndex == 0 && velocity.x > 0) {
|
|
||||||
+ self.scrollView.panGestureRecognizer.enabled = false;
|
|
||||||
+ return NO;
|
|
||||||
+ } else {
|
|
||||||
+ self.scrollView.panGestureRecognizer.enabled = self.scrollEnabled;
|
|
||||||
+ }
|
|
||||||
+ } else {
|
|
||||||
+ self.scrollView.panGestureRecognizer.enabled = self.scrollEnabled;
|
|
||||||
+ }
|
|
||||||
+
|
|
||||||
+ return YES;
|
|
||||||
+}
|
|
||||||
@end
|
|
||||||
@@ -39,6 +39,8 @@ import {
|
|||||||
import {Provider as UnreadNotifsProvider} from 'state/queries/notifications/unread'
|
import {Provider as UnreadNotifsProvider} from 'state/queries/notifications/unread'
|
||||||
import * as persisted from '#/state/persisted'
|
import * as persisted from '#/state/persisted'
|
||||||
import {Splash} from '#/Splash'
|
import {Splash} from '#/Splash'
|
||||||
|
import {msg} from '@lingui/macro'
|
||||||
|
import {useLingui} from '@lingui/react'
|
||||||
|
|
||||||
SplashScreen.preventAutoHideAsync()
|
SplashScreen.preventAutoHideAsync()
|
||||||
|
|
||||||
@@ -46,17 +48,18 @@ function InnerApp() {
|
|||||||
const colorMode = useColorMode()
|
const colorMode = useColorMode()
|
||||||
const {isInitialLoad, currentAccount} = useSession()
|
const {isInitialLoad, currentAccount} = useSession()
|
||||||
const {resumeSession} = useSessionApi()
|
const {resumeSession} = useSessionApi()
|
||||||
|
const {_} = useLingui()
|
||||||
|
|
||||||
// init
|
// init
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
notifications.init(queryClient)
|
notifications.init(queryClient)
|
||||||
listenSessionDropped(() => {
|
listenSessionDropped(() => {
|
||||||
Toast.show('Sorry! Your session expired. Please log in again.')
|
Toast.show(_(msg`Sorry! Your session expired. Please log in again.`))
|
||||||
})
|
})
|
||||||
|
|
||||||
const account = persisted.get('session').currentAccount
|
const account = persisted.get('session').currentAccount
|
||||||
resumeSession(account)
|
resumeSession(account)
|
||||||
}, [resumeSession])
|
}, [resumeSession, _])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SafeAreaProvider initialMetrics={initialWindowMetrics}>
|
<SafeAreaProvider initialMetrics={initialWindowMetrics}>
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import {RootSiblingParent} from 'react-native-root-siblings'
|
|||||||
|
|
||||||
import 'view/icons'
|
import 'view/icons'
|
||||||
|
|
||||||
|
import {ThemeProvider as Alf} from '#/alf'
|
||||||
import {init as initPersistedState} from '#/state/persisted'
|
import {init as initPersistedState} from '#/state/persisted'
|
||||||
import {useColorMode} from 'state/shell'
|
import {useColorMode} from 'state/shell'
|
||||||
import {Shell} from 'view/shell/index'
|
import {Shell} from 'view/shell/index'
|
||||||
@@ -28,11 +29,13 @@ import {
|
|||||||
} from 'state/session'
|
} from 'state/session'
|
||||||
import {Provider as UnreadNotifsProvider} from 'state/queries/notifications/unread'
|
import {Provider as UnreadNotifsProvider} from 'state/queries/notifications/unread'
|
||||||
import * as persisted from '#/state/persisted'
|
import * as persisted from '#/state/persisted'
|
||||||
|
import {useColorModeTheme} from '#/alf/util/useColorModeTheme'
|
||||||
|
|
||||||
function InnerApp() {
|
function InnerApp() {
|
||||||
const {isInitialLoad, currentAccount} = useSession()
|
const {isInitialLoad, currentAccount} = useSession()
|
||||||
const {resumeSession} = useSessionApi()
|
const {resumeSession} = useSessionApi()
|
||||||
const colorMode = useColorMode()
|
const colorMode = useColorMode()
|
||||||
|
const theme = useColorModeTheme(colorMode)
|
||||||
|
|
||||||
// init
|
// init
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -44,23 +47,25 @@ function InnerApp() {
|
|||||||
if (isInitialLoad) return null
|
if (isInitialLoad) return null
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<React.Fragment
|
<Alf theme={theme}>
|
||||||
// Resets the entire tree below when it changes:
|
<React.Fragment
|
||||||
key={currentAccount?.did}>
|
// Resets the entire tree below when it changes:
|
||||||
<LoggedOutViewProvider>
|
key={currentAccount?.did}>
|
||||||
<UnreadNotifsProvider>
|
<LoggedOutViewProvider>
|
||||||
<ThemeProvider theme={colorMode}>
|
<UnreadNotifsProvider>
|
||||||
{/* All components should be within this provider */}
|
<ThemeProvider theme={colorMode}>
|
||||||
<RootSiblingParent>
|
{/* All components should be within this provider */}
|
||||||
<SafeAreaProvider>
|
<RootSiblingParent>
|
||||||
<Shell />
|
<SafeAreaProvider>
|
||||||
</SafeAreaProvider>
|
<Shell />
|
||||||
</RootSiblingParent>
|
</SafeAreaProvider>
|
||||||
<ToastContainer />
|
</RootSiblingParent>
|
||||||
</ThemeProvider>
|
<ToastContainer />
|
||||||
</UnreadNotifsProvider>
|
</ThemeProvider>
|
||||||
</LoggedOutViewProvider>
|
</UnreadNotifsProvider>
|
||||||
</React.Fragment>
|
</LoggedOutViewProvider>
|
||||||
|
</React.Fragment>
|
||||||
|
</Alf>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ import {BottomBar} from './view/shell/bottom-bar/BottomBar'
|
|||||||
import {buildStateObject} from 'lib/routes/helpers'
|
import {buildStateObject} from 'lib/routes/helpers'
|
||||||
import {State, RouteParams} from 'lib/routes/types'
|
import {State, RouteParams} from 'lib/routes/types'
|
||||||
import {colors} from 'lib/styles'
|
import {colors} from 'lib/styles'
|
||||||
import {isNative} from 'platform/detection'
|
import {isAndroid, isNative} from 'platform/detection'
|
||||||
import {useColorSchemeStyle} from 'lib/hooks/useColorSchemeStyle'
|
import {useColorSchemeStyle} from 'lib/hooks/useColorSchemeStyle'
|
||||||
import {router} from './routes'
|
import {router} from './routes'
|
||||||
import {usePalette} from 'lib/hooks/usePalette'
|
import {usePalette} from 'lib/hooks/usePalette'
|
||||||
@@ -62,7 +62,7 @@ import {ProfileListScreen} from './view/screens/ProfileList'
|
|||||||
import {PostThreadScreen} from './view/screens/PostThread'
|
import {PostThreadScreen} from './view/screens/PostThread'
|
||||||
import {PostLikedByScreen} from './view/screens/PostLikedBy'
|
import {PostLikedByScreen} from './view/screens/PostLikedBy'
|
||||||
import {PostRepostedByScreen} from './view/screens/PostRepostedBy'
|
import {PostRepostedByScreen} from './view/screens/PostRepostedBy'
|
||||||
import {DebugScreen} from './view/screens/Debug'
|
import {DebugScreen} from './view/screens/DebugNew'
|
||||||
import {LogScreen} from './view/screens/Log'
|
import {LogScreen} from './view/screens/Log'
|
||||||
import {SupportScreen} from './view/screens/Support'
|
import {SupportScreen} from './view/screens/Support'
|
||||||
import {PrivacyPolicyScreen} from './view/screens/PrivacyPolicy'
|
import {PrivacyPolicyScreen} from './view/screens/PrivacyPolicy'
|
||||||
@@ -75,7 +75,10 @@ import {ModerationBlockedAccounts} from 'view/screens/ModerationBlockedAccounts'
|
|||||||
import {SavedFeeds} from 'view/screens/SavedFeeds'
|
import {SavedFeeds} from 'view/screens/SavedFeeds'
|
||||||
import {PreferencesHomeFeed} from 'view/screens/PreferencesHomeFeed'
|
import {PreferencesHomeFeed} from 'view/screens/PreferencesHomeFeed'
|
||||||
import {PreferencesThreads} from 'view/screens/PreferencesThreads'
|
import {PreferencesThreads} from 'view/screens/PreferencesThreads'
|
||||||
|
import {PreferencesExternalEmbeds} from '#/view/screens/PreferencesExternalEmbeds'
|
||||||
import {createNativeStackNavigatorWithAuth} from './view/shell/createNativeStackNavigatorWithAuth'
|
import {createNativeStackNavigatorWithAuth} from './view/shell/createNativeStackNavigatorWithAuth'
|
||||||
|
import {msg} from '@lingui/macro'
|
||||||
|
import {i18n, MessageDescriptor} from '@lingui/core'
|
||||||
|
|
||||||
const navigationRef = createNavigationContainerRef<AllNavigatorParams>()
|
const navigationRef = createNavigationContainerRef<AllNavigatorParams>()
|
||||||
|
|
||||||
@@ -93,55 +96,56 @@ const Tab = createBottomTabNavigator<BottomTabNavigatorParams>()
|
|||||||
* These "common screens" are reused across stacks.
|
* These "common screens" are reused across stacks.
|
||||||
*/
|
*/
|
||||||
function commonScreens(Stack: typeof HomeTab, unreadCountLabel?: string) {
|
function commonScreens(Stack: typeof HomeTab, unreadCountLabel?: string) {
|
||||||
const title = (page: string) => bskyTitle(page, unreadCountLabel)
|
const title = (page: MessageDescriptor) =>
|
||||||
|
bskyTitle(i18n._(page), unreadCountLabel)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
name="NotFound"
|
name="NotFound"
|
||||||
getComponent={() => NotFoundScreen}
|
getComponent={() => NotFoundScreen}
|
||||||
options={{title: title('Not Found')}}
|
options={{title: title(msg`Not Found`)}}
|
||||||
/>
|
/>
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
name="Lists"
|
name="Lists"
|
||||||
component={ListsScreen}
|
component={ListsScreen}
|
||||||
options={{title: title('Lists'), requireAuth: true}}
|
options={{title: title(msg`Lists`), requireAuth: true}}
|
||||||
/>
|
/>
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
name="Moderation"
|
name="Moderation"
|
||||||
getComponent={() => ModerationScreen}
|
getComponent={() => ModerationScreen}
|
||||||
options={{title: title('Moderation'), requireAuth: true}}
|
options={{title: title(msg`Moderation`), requireAuth: true}}
|
||||||
/>
|
/>
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
name="ModerationModlists"
|
name="ModerationModlists"
|
||||||
getComponent={() => ModerationModlistsScreen}
|
getComponent={() => ModerationModlistsScreen}
|
||||||
options={{title: title('Moderation Lists'), requireAuth: true}}
|
options={{title: title(msg`Moderation Lists`), requireAuth: true}}
|
||||||
/>
|
/>
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
name="ModerationMutedAccounts"
|
name="ModerationMutedAccounts"
|
||||||
getComponent={() => ModerationMutedAccounts}
|
getComponent={() => ModerationMutedAccounts}
|
||||||
options={{title: title('Muted Accounts'), requireAuth: true}}
|
options={{title: title(msg`Muted Accounts`), requireAuth: true}}
|
||||||
/>
|
/>
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
name="ModerationBlockedAccounts"
|
name="ModerationBlockedAccounts"
|
||||||
getComponent={() => ModerationBlockedAccounts}
|
getComponent={() => ModerationBlockedAccounts}
|
||||||
options={{title: title('Blocked Accounts'), requireAuth: true}}
|
options={{title: title(msg`Blocked Accounts`), requireAuth: true}}
|
||||||
/>
|
/>
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
name="Settings"
|
name="Settings"
|
||||||
getComponent={() => SettingsScreen}
|
getComponent={() => SettingsScreen}
|
||||||
options={{title: title('Settings'), requireAuth: true}}
|
options={{title: title(msg`Settings`), requireAuth: true}}
|
||||||
/>
|
/>
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
name="LanguageSettings"
|
name="LanguageSettings"
|
||||||
getComponent={() => LanguageSettingsScreen}
|
getComponent={() => LanguageSettingsScreen}
|
||||||
options={{title: title('Language Settings'), requireAuth: true}}
|
options={{title: title(msg`Language Settings`), requireAuth: true}}
|
||||||
/>
|
/>
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
name="Profile"
|
name="Profile"
|
||||||
getComponent={() => ProfileScreen}
|
getComponent={() => ProfileScreen}
|
||||||
options={({route}) => ({
|
options={({route}) => ({
|
||||||
title: title(`@${route.params.name}`),
|
title: title(msg`@${route.params.name}`),
|
||||||
animation: 'none',
|
animation: 'none',
|
||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
@@ -149,100 +153,114 @@ function commonScreens(Stack: typeof HomeTab, unreadCountLabel?: string) {
|
|||||||
name="ProfileFollowers"
|
name="ProfileFollowers"
|
||||||
getComponent={() => ProfileFollowersScreen}
|
getComponent={() => ProfileFollowersScreen}
|
||||||
options={({route}) => ({
|
options={({route}) => ({
|
||||||
title: title(`People following @${route.params.name}`),
|
title: title(msg`People following @${route.params.name}`),
|
||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
name="ProfileFollows"
|
name="ProfileFollows"
|
||||||
getComponent={() => ProfileFollowsScreen}
|
getComponent={() => ProfileFollowsScreen}
|
||||||
options={({route}) => ({
|
options={({route}) => ({
|
||||||
title: title(`People followed by @${route.params.name}`),
|
title: title(msg`People followed by @${route.params.name}`),
|
||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
name="ProfileList"
|
name="ProfileList"
|
||||||
getComponent={() => ProfileListScreen}
|
getComponent={() => ProfileListScreen}
|
||||||
options={{title: title('List'), requireAuth: true}}
|
options={{title: title(msg`List`), requireAuth: true}}
|
||||||
/>
|
/>
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
name="PostThread"
|
name="PostThread"
|
||||||
getComponent={() => PostThreadScreen}
|
getComponent={() => PostThreadScreen}
|
||||||
options={({route}) => ({title: title(`Post by @${route.params.name}`)})}
|
options={({route}) => ({
|
||||||
|
title: title(msg`Post by @${route.params.name}`),
|
||||||
|
})}
|
||||||
/>
|
/>
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
name="PostLikedBy"
|
name="PostLikedBy"
|
||||||
getComponent={() => PostLikedByScreen}
|
getComponent={() => PostLikedByScreen}
|
||||||
options={({route}) => ({title: title(`Post by @${route.params.name}`)})}
|
options={({route}) => ({
|
||||||
|
title: title(msg`Post by @${route.params.name}`),
|
||||||
|
})}
|
||||||
/>
|
/>
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
name="PostRepostedBy"
|
name="PostRepostedBy"
|
||||||
getComponent={() => PostRepostedByScreen}
|
getComponent={() => PostRepostedByScreen}
|
||||||
options={({route}) => ({title: title(`Post by @${route.params.name}`)})}
|
options={({route}) => ({
|
||||||
|
title: title(msg`Post by @${route.params.name}`),
|
||||||
|
})}
|
||||||
/>
|
/>
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
name="ProfileFeed"
|
name="ProfileFeed"
|
||||||
getComponent={() => ProfileFeedScreen}
|
getComponent={() => ProfileFeedScreen}
|
||||||
options={{title: title('Feed'), requireAuth: true}}
|
options={{title: title(msg`Feed`), requireAuth: true}}
|
||||||
/>
|
/>
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
name="ProfileFeedLikedBy"
|
name="ProfileFeedLikedBy"
|
||||||
getComponent={() => ProfileFeedLikedByScreen}
|
getComponent={() => ProfileFeedLikedByScreen}
|
||||||
options={{title: title('Liked by')}}
|
options={{title: title(msg`Liked by`)}}
|
||||||
/>
|
/>
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
name="Debug"
|
name="Debug"
|
||||||
getComponent={() => DebugScreen}
|
getComponent={() => DebugScreen}
|
||||||
options={{title: title('Debug'), requireAuth: true}}
|
options={{title: title(msg`Debug`), requireAuth: true}}
|
||||||
/>
|
/>
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
name="Log"
|
name="Log"
|
||||||
getComponent={() => LogScreen}
|
getComponent={() => LogScreen}
|
||||||
options={{title: title('Log'), requireAuth: true}}
|
options={{title: title(msg`Log`), requireAuth: true}}
|
||||||
/>
|
/>
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
name="Support"
|
name="Support"
|
||||||
getComponent={() => SupportScreen}
|
getComponent={() => SupportScreen}
|
||||||
options={{title: title('Support')}}
|
options={{title: title(msg`Support`)}}
|
||||||
/>
|
/>
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
name="PrivacyPolicy"
|
name="PrivacyPolicy"
|
||||||
getComponent={() => PrivacyPolicyScreen}
|
getComponent={() => PrivacyPolicyScreen}
|
||||||
options={{title: title('Privacy Policy')}}
|
options={{title: title(msg`Privacy Policy`)}}
|
||||||
/>
|
/>
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
name="TermsOfService"
|
name="TermsOfService"
|
||||||
getComponent={() => TermsOfServiceScreen}
|
getComponent={() => TermsOfServiceScreen}
|
||||||
options={{title: title('Terms of Service')}}
|
options={{title: title(msg`Terms of Service`)}}
|
||||||
/>
|
/>
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
name="CommunityGuidelines"
|
name="CommunityGuidelines"
|
||||||
getComponent={() => CommunityGuidelinesScreen}
|
getComponent={() => CommunityGuidelinesScreen}
|
||||||
options={{title: title('Community Guidelines')}}
|
options={{title: title(msg`Community Guidelines`)}}
|
||||||
/>
|
/>
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
name="CopyrightPolicy"
|
name="CopyrightPolicy"
|
||||||
getComponent={() => CopyrightPolicyScreen}
|
getComponent={() => CopyrightPolicyScreen}
|
||||||
options={{title: title('Copyright Policy')}}
|
options={{title: title(msg`Copyright Policy`)}}
|
||||||
/>
|
/>
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
name="AppPasswords"
|
name="AppPasswords"
|
||||||
getComponent={() => AppPasswords}
|
getComponent={() => AppPasswords}
|
||||||
options={{title: title('App Passwords'), requireAuth: true}}
|
options={{title: title(msg`App Passwords`), requireAuth: true}}
|
||||||
/>
|
/>
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
name="SavedFeeds"
|
name="SavedFeeds"
|
||||||
getComponent={() => SavedFeeds}
|
getComponent={() => SavedFeeds}
|
||||||
options={{title: title('Edit My Feeds'), requireAuth: true}}
|
options={{title: title(msg`Edit My Feeds`), requireAuth: true}}
|
||||||
/>
|
/>
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
name="PreferencesHomeFeed"
|
name="PreferencesHomeFeed"
|
||||||
getComponent={() => PreferencesHomeFeed}
|
getComponent={() => PreferencesHomeFeed}
|
||||||
options={{title: title('Home Feed Preferences'), requireAuth: true}}
|
options={{title: title(msg`Home Feed Preferences`), requireAuth: true}}
|
||||||
/>
|
/>
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
name="PreferencesThreads"
|
name="PreferencesThreads"
|
||||||
getComponent={() => PreferencesThreads}
|
getComponent={() => PreferencesThreads}
|
||||||
options={{title: title('Threads Preferences'), requireAuth: true}}
|
options={{title: title(msg`Threads Preferences`), requireAuth: true}}
|
||||||
|
/>
|
||||||
|
<Stack.Screen
|
||||||
|
name="PreferencesExternalEmbeds"
|
||||||
|
getComponent={() => PreferencesExternalEmbeds}
|
||||||
|
options={{
|
||||||
|
title: title(msg`External Media Preferences`),
|
||||||
|
requireAuth: true,
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
@@ -287,6 +305,7 @@ function HomeTabNavigator() {
|
|||||||
return (
|
return (
|
||||||
<HomeTab.Navigator
|
<HomeTab.Navigator
|
||||||
screenOptions={{
|
screenOptions={{
|
||||||
|
animation: isAndroid ? 'none' : undefined,
|
||||||
gestureEnabled: true,
|
gestureEnabled: true,
|
||||||
fullScreenGestureEnabled: true,
|
fullScreenGestureEnabled: true,
|
||||||
headerShown: false,
|
headerShown: false,
|
||||||
@@ -308,6 +327,7 @@ function SearchTabNavigator() {
|
|||||||
return (
|
return (
|
||||||
<SearchTab.Navigator
|
<SearchTab.Navigator
|
||||||
screenOptions={{
|
screenOptions={{
|
||||||
|
animation: isAndroid ? 'none' : undefined,
|
||||||
gestureEnabled: true,
|
gestureEnabled: true,
|
||||||
fullScreenGestureEnabled: true,
|
fullScreenGestureEnabled: true,
|
||||||
headerShown: false,
|
headerShown: false,
|
||||||
@@ -325,6 +345,7 @@ function FeedsTabNavigator() {
|
|||||||
return (
|
return (
|
||||||
<FeedsTab.Navigator
|
<FeedsTab.Navigator
|
||||||
screenOptions={{
|
screenOptions={{
|
||||||
|
animation: isAndroid ? 'none' : undefined,
|
||||||
gestureEnabled: true,
|
gestureEnabled: true,
|
||||||
fullScreenGestureEnabled: true,
|
fullScreenGestureEnabled: true,
|
||||||
headerShown: false,
|
headerShown: false,
|
||||||
@@ -346,6 +367,7 @@ function NotificationsTabNavigator() {
|
|||||||
return (
|
return (
|
||||||
<NotificationsTab.Navigator
|
<NotificationsTab.Navigator
|
||||||
screenOptions={{
|
screenOptions={{
|
||||||
|
animation: isAndroid ? 'none' : undefined,
|
||||||
gestureEnabled: true,
|
gestureEnabled: true,
|
||||||
fullScreenGestureEnabled: true,
|
fullScreenGestureEnabled: true,
|
||||||
headerShown: false,
|
headerShown: false,
|
||||||
@@ -367,6 +389,7 @@ function MyProfileTabNavigator() {
|
|||||||
return (
|
return (
|
||||||
<MyProfileTab.Navigator
|
<MyProfileTab.Navigator
|
||||||
screenOptions={{
|
screenOptions={{
|
||||||
|
animation: isAndroid ? 'none' : undefined,
|
||||||
gestureEnabled: true,
|
gestureEnabled: true,
|
||||||
fullScreenGestureEnabled: true,
|
fullScreenGestureEnabled: true,
|
||||||
headerShown: false,
|
headerShown: false,
|
||||||
@@ -394,7 +417,7 @@ const FlatNavigator = () => {
|
|||||||
const pal = usePalette('default')
|
const pal = usePalette('default')
|
||||||
const numUnread = useUnreadNotifications()
|
const numUnread = useUnreadNotifications()
|
||||||
const screenListeners = useWebScrollRestoration()
|
const screenListeners = useWebScrollRestoration()
|
||||||
const title = (page: string) => bskyTitle(page, numUnread)
|
const title = (page: MessageDescriptor) => bskyTitle(i18n._(page), numUnread)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Flat.Navigator
|
<Flat.Navigator
|
||||||
@@ -409,22 +432,22 @@ const FlatNavigator = () => {
|
|||||||
<Flat.Screen
|
<Flat.Screen
|
||||||
name="Home"
|
name="Home"
|
||||||
getComponent={() => HomeScreen}
|
getComponent={() => HomeScreen}
|
||||||
options={{title: title('Home'), requireAuth: true}}
|
options={{title: title(msg`Home`), requireAuth: true}}
|
||||||
/>
|
/>
|
||||||
<Flat.Screen
|
<Flat.Screen
|
||||||
name="Search"
|
name="Search"
|
||||||
getComponent={() => SearchScreen}
|
getComponent={() => SearchScreen}
|
||||||
options={{title: title('Search')}}
|
options={{title: title(msg`Search`)}}
|
||||||
/>
|
/>
|
||||||
<Flat.Screen
|
<Flat.Screen
|
||||||
name="Feeds"
|
name="Feeds"
|
||||||
getComponent={() => FeedsScreen}
|
getComponent={() => FeedsScreen}
|
||||||
options={{title: title('Feeds'), requireAuth: true}}
|
options={{title: title(msg`Feeds`), requireAuth: true}}
|
||||||
/>
|
/>
|
||||||
<Flat.Screen
|
<Flat.Screen
|
||||||
name="Notifications"
|
name="Notifications"
|
||||||
getComponent={() => NotificationsScreen}
|
getComponent={() => NotificationsScreen}
|
||||||
options={{title: title('Notifications'), requireAuth: true}}
|
options={{title: title(msg`Notifications`), requireAuth: true}}
|
||||||
/>
|
/>
|
||||||
{commonScreens(Flat as typeof HomeTab, numUnread)}
|
{commonScreens(Flat as typeof HomeTab, numUnread)}
|
||||||
</Flat.Navigator>
|
</Flat.Navigator>
|
||||||
|
|||||||
@@ -1,5 +1,11 @@
|
|||||||
import React, {useCallback, useEffect} from 'react'
|
import React, {useCallback, useEffect} from 'react'
|
||||||
import {View, StyleSheet, Image as RNImage} from 'react-native'
|
import {
|
||||||
|
View,
|
||||||
|
StyleSheet,
|
||||||
|
Image as RNImage,
|
||||||
|
AccessibilityInfo,
|
||||||
|
useColorScheme,
|
||||||
|
} from 'react-native'
|
||||||
import * as SplashScreen from 'expo-splash-screen'
|
import * as SplashScreen from 'expo-splash-screen'
|
||||||
import {Image} from 'expo-image'
|
import {Image} from 'expo-image'
|
||||||
import Animated, {
|
import Animated, {
|
||||||
@@ -14,9 +20,18 @@ import MaskedView from '@react-native-masked-view/masked-view'
|
|||||||
import {useSafeAreaInsets} from 'react-native-safe-area-context'
|
import {useSafeAreaInsets} from 'react-native-safe-area-context'
|
||||||
import Svg, {Path, SvgProps} from 'react-native-svg'
|
import Svg, {Path, SvgProps} from 'react-native-svg'
|
||||||
|
|
||||||
|
import {isAndroid} from '#/platform/detection'
|
||||||
|
import {useColorMode} from 'state/shell'
|
||||||
|
import {colors} from '#/lib/styles'
|
||||||
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
import splashImagePointer from '../assets/splash.png'
|
import splashImagePointer from '../assets/splash.png'
|
||||||
|
// @ts-ignore
|
||||||
|
import darkSplashImagePointer from '../assets/splash-dark.png'
|
||||||
const splashImageUri = RNImage.resolveAssetSource(splashImagePointer).uri
|
const splashImageUri = RNImage.resolveAssetSource(splashImagePointer).uri
|
||||||
|
const darkSplashImageUri = RNImage.resolveAssetSource(
|
||||||
|
darkSplashImagePointer,
|
||||||
|
).uri
|
||||||
|
|
||||||
export const Logo = React.forwardRef(function LogoImpl(props: SvgProps, ref) {
|
export const Logo = React.forwardRef(function LogoImpl(props: SvgProps, ref) {
|
||||||
const width = 1000
|
const width = 1000
|
||||||
@@ -27,9 +42,9 @@ export const Logo = React.forwardRef(function LogoImpl(props: SvgProps, ref) {
|
|||||||
// @ts-ignore it's fiiiiine
|
// @ts-ignore it's fiiiiine
|
||||||
ref={ref}
|
ref={ref}
|
||||||
viewBox="0 0 64 66"
|
viewBox="0 0 64 66"
|
||||||
style={{width, height}}>
|
style={[{width, height}, props.style]}>
|
||||||
<Path
|
<Path
|
||||||
fill="#fff"
|
fill={props.fill || '#fff'}
|
||||||
d="M13.873 3.77C21.21 9.243 29.103 20.342 32 26.3v15.732c0-.335-.13.043-.41.858-1.512 4.414-7.418 21.642-20.923 7.87-7.111-7.252-3.819-14.503 9.125-16.692-7.405 1.252-15.73-.817-18.014-8.93C1.12 22.804 0 8.431 0 6.488 0-3.237 8.579-.18 13.873 3.77ZM50.127 3.77C42.79 9.243 34.897 20.342 32 26.3v15.732c0-.335.13.043.41.858 1.512 4.414 7.418 21.642 20.923 7.87 7.111-7.252 3.819-14.503-9.125-16.692 7.405 1.252 15.73-.817 18.014-8.93C62.88 22.804 64 8.431 64 6.488 64-3.237 55.422-.18 50.127 3.77Z"
|
d="M13.873 3.77C21.21 9.243 29.103 20.342 32 26.3v15.732c0-.335-.13.043-.41.858-1.512 4.414-7.418 21.642-20.923 7.87-7.111-7.252-3.819-14.503 9.125-16.692-7.405 1.252-15.73-.817-18.014-8.93C1.12 22.804 0 8.431 0 6.488 0-3.237 8.579-.18 13.873 3.77ZM50.127 3.77C42.79 9.243 34.897 20.342 32 26.3v15.732c0-.335.13.043.41.858 1.512 4.414 7.418 21.642 20.923 7.87 7.111-7.252 3.819-14.503-9.125-16.692 7.405 1.252 15.73-.817 18.014-8.93C62.88 22.804 64 8.431 64 6.488 64-3.237 55.422-.18 50.127 3.77Z"
|
||||||
/>
|
/>
|
||||||
</Svg>
|
</Svg>
|
||||||
@@ -40,8 +55,6 @@ type Props = {
|
|||||||
isReady: boolean
|
isReady: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
SplashScreen.preventAutoHideAsync().catch(() => {})
|
|
||||||
|
|
||||||
const AnimatedLogo = Animated.createAnimatedComponent(Logo)
|
const AnimatedLogo = Animated.createAnimatedComponent(Logo)
|
||||||
|
|
||||||
export function Splash(props: React.PropsWithChildren<Props>) {
|
export function Splash(props: React.PropsWithChildren<Props>) {
|
||||||
@@ -52,9 +65,22 @@ export function Splash(props: React.PropsWithChildren<Props>) {
|
|||||||
const outroAppOpacity = useSharedValue(0)
|
const outroAppOpacity = useSharedValue(0)
|
||||||
const [isAnimationComplete, setIsAnimationComplete] = React.useState(false)
|
const [isAnimationComplete, setIsAnimationComplete] = React.useState(false)
|
||||||
const [isImageLoaded, setIsImageLoaded] = React.useState(false)
|
const [isImageLoaded, setIsImageLoaded] = React.useState(false)
|
||||||
const isReady = props.isReady && isImageLoaded
|
const [isLayoutReady, setIsLayoutReady] = React.useState(false)
|
||||||
|
const [reduceMotion, setReduceMotion] = React.useState<boolean | undefined>(
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
const isReady =
|
||||||
|
props.isReady &&
|
||||||
|
isImageLoaded &&
|
||||||
|
isLayoutReady &&
|
||||||
|
reduceMotion !== undefined
|
||||||
|
|
||||||
const logoAnimations = useAnimatedStyle(() => {
|
const colorMode = useColorMode()
|
||||||
|
const colorScheme = useColorScheme()
|
||||||
|
const themeName = colorMode === 'system' ? colorScheme : colorMode
|
||||||
|
const isDarkMode = themeName === 'dark'
|
||||||
|
|
||||||
|
const logoAnimation = useAnimatedStyle(() => {
|
||||||
return {
|
return {
|
||||||
transform: [
|
transform: [
|
||||||
{
|
{
|
||||||
@@ -64,7 +90,7 @@ export function Splash(props: React.PropsWithChildren<Props>) {
|
|||||||
scale: interpolate(
|
scale: interpolate(
|
||||||
outroLogo.value,
|
outroLogo.value,
|
||||||
[0, 0.08, 1],
|
[0, 0.08, 1],
|
||||||
[1, 0.8, 400],
|
[1, 0.8, 500],
|
||||||
'clamp',
|
'clamp',
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
@@ -72,6 +98,27 @@ export function Splash(props: React.PropsWithChildren<Props>) {
|
|||||||
opacity: interpolate(intro.value, [0, 1], [0, 1], 'clamp'),
|
opacity: interpolate(intro.value, [0, 1], [0, 1], 'clamp'),
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
const reducedLogoAnimation = useAnimatedStyle(() => {
|
||||||
|
return {
|
||||||
|
transform: [
|
||||||
|
{
|
||||||
|
scale: interpolate(intro.value, [0, 1], [0.8, 1], 'clamp'),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
opacity: interpolate(intro.value, [0, 1], [0, 1], 'clamp'),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const logoWrapperAnimation = useAnimatedStyle(() => {
|
||||||
|
return {
|
||||||
|
opacity: interpolate(
|
||||||
|
outroAppOpacity.value,
|
||||||
|
[0, 0.1, 0.2, 1],
|
||||||
|
[1, 1, 0, 0],
|
||||||
|
'clamp',
|
||||||
|
),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
const appAnimation = useAnimatedStyle(() => {
|
const appAnimation = useAnimatedStyle(() => {
|
||||||
return {
|
return {
|
||||||
@@ -82,7 +129,7 @@ export function Splash(props: React.PropsWithChildren<Props>) {
|
|||||||
],
|
],
|
||||||
opacity: interpolate(
|
opacity: interpolate(
|
||||||
outroAppOpacity.value,
|
outroAppOpacity.value,
|
||||||
[0, 0.08, 0.15, 1],
|
[0, 0.1, 0.2, 1],
|
||||||
[0, 0, 1, 1],
|
[0, 0, 1, 1],
|
||||||
'clamp',
|
'clamp',
|
||||||
),
|
),
|
||||||
@@ -90,82 +137,121 @@ export function Splash(props: React.PropsWithChildren<Props>) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const onFinish = useCallback(() => setIsAnimationComplete(true), [])
|
const onFinish = useCallback(() => setIsAnimationComplete(true), [])
|
||||||
|
const onLayout = useCallback(() => setIsLayoutReady(true), [])
|
||||||
|
const onLoadEnd = useCallback(() => setIsImageLoaded(true), [])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isReady) {
|
if (isReady) {
|
||||||
// hide on mount
|
SplashScreen.hideAsync()
|
||||||
SplashScreen.hideAsync().catch(() => {})
|
.then(() => {
|
||||||
|
intro.value = withTiming(
|
||||||
intro.value = withTiming(
|
|
||||||
1,
|
|
||||||
{duration: 400, easing: Easing.out(Easing.cubic)},
|
|
||||||
async () => {
|
|
||||||
// set these values to check animation at specific point
|
|
||||||
// outroLogo.value = 0.1
|
|
||||||
// outroApp.value = 0.1
|
|
||||||
outroLogo.value = withTiming(
|
|
||||||
1,
|
1,
|
||||||
{duration: 1200, easing: Easing.in(Easing.cubic)},
|
{duration: 400, easing: Easing.out(Easing.cubic)},
|
||||||
() => {
|
async () => {
|
||||||
runOnJS(onFinish)()
|
// set these values to check animation at specific point
|
||||||
|
// outroLogo.value = 0.1
|
||||||
|
// outroApp.value = 0.1
|
||||||
|
outroLogo.value = withTiming(
|
||||||
|
1,
|
||||||
|
{duration: 1200, easing: Easing.in(Easing.cubic)},
|
||||||
|
() => {
|
||||||
|
runOnJS(onFinish)()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
outroApp.value = withTiming(1, {
|
||||||
|
duration: 1200,
|
||||||
|
easing: Easing.inOut(Easing.cubic),
|
||||||
|
})
|
||||||
|
outroAppOpacity.value = withTiming(1, {
|
||||||
|
duration: 1200,
|
||||||
|
easing: Easing.in(Easing.cubic),
|
||||||
|
})
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
outroApp.value = withTiming(1, {
|
})
|
||||||
duration: 1200,
|
.catch(() => {})
|
||||||
easing: Easing.inOut(Easing.cubic),
|
|
||||||
})
|
|
||||||
outroAppOpacity.value = withTiming(1, {
|
|
||||||
duration: 1200,
|
|
||||||
easing: Easing.in(Easing.cubic),
|
|
||||||
})
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}, [onFinish, intro, outroLogo, outroApp, outroAppOpacity, isReady])
|
}, [onFinish, intro, outroLogo, outroApp, outroAppOpacity, isReady])
|
||||||
|
|
||||||
const onLoadEnd = useCallback(() => {
|
useEffect(() => {
|
||||||
setIsImageLoaded(true)
|
AccessibilityInfo.isReduceMotionEnabled().then(setReduceMotion)
|
||||||
}, [setIsImageLoaded])
|
}, [])
|
||||||
|
|
||||||
|
const logoAnimations =
|
||||||
|
reduceMotion === true ? reducedLogoAnimation : logoAnimation
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={{flex: 1}}>
|
<View style={{flex: 1}} onLayout={onLayout}>
|
||||||
{!isAnimationComplete && (
|
{!isAnimationComplete && (
|
||||||
<Image
|
<Image
|
||||||
accessibilityIgnoresInvertColors
|
accessibilityIgnoresInvertColors
|
||||||
onLoadEnd={onLoadEnd}
|
onLoadEnd={onLoadEnd}
|
||||||
source={{uri: splashImageUri}}
|
source={{uri: isDarkMode ? darkSplashImageUri : splashImageUri}}
|
||||||
style={StyleSheet.absoluteFillObject}
|
style={StyleSheet.absoluteFillObject}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<MaskedView
|
{isReady &&
|
||||||
style={[StyleSheet.absoluteFillObject]}
|
(isAndroid || reduceMotion === true ? (
|
||||||
maskElement={
|
// Use a simple fade on older versions of android (work around a bug)
|
||||||
<Animated.View
|
<>
|
||||||
style={[
|
<Animated.View style={[{flex: 1}, appAnimation]}>
|
||||||
StyleSheet.absoluteFillObject,
|
{props.children}
|
||||||
{
|
</Animated.View>
|
||||||
// Transparent background because mask is based off alpha channel.
|
|
||||||
backgroundColor: 'transparent',
|
|
||||||
flex: 1,
|
|
||||||
justifyContent: 'center',
|
|
||||||
alignItems: 'center',
|
|
||||||
transform: [{translateY: -(insets.top / 2)}, {scale: 0.1}], // scale from 1000px to 100px
|
|
||||||
},
|
|
||||||
]}>
|
|
||||||
<AnimatedLogo style={[logoAnimations]} />
|
|
||||||
</Animated.View>
|
|
||||||
}>
|
|
||||||
{!isAnimationComplete && (
|
|
||||||
<View
|
|
||||||
style={[StyleSheet.absoluteFillObject, {backgroundColor: 'white'}]}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Animated.View style={[{flex: 1}, appAnimation]}>
|
{!isAnimationComplete && (
|
||||||
{props.children}
|
<Animated.View
|
||||||
</Animated.View>
|
style={[
|
||||||
</MaskedView>
|
StyleSheet.absoluteFillObject,
|
||||||
|
logoWrapperAnimation,
|
||||||
|
{
|
||||||
|
flex: 1,
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
transform: [{translateY: -(insets.top / 2)}, {scale: 0.1}], // scale from 1000px to 100px
|
||||||
|
},
|
||||||
|
]}>
|
||||||
|
<AnimatedLogo
|
||||||
|
fill={isDarkMode ? colors.blue3 : '#fff'}
|
||||||
|
style={[{opacity: 0}, logoAnimations]}
|
||||||
|
/>
|
||||||
|
</Animated.View>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<MaskedView
|
||||||
|
style={[StyleSheet.absoluteFillObject]}
|
||||||
|
maskElement={
|
||||||
|
<Animated.View
|
||||||
|
style={[
|
||||||
|
{
|
||||||
|
// Transparent background because mask is based off alpha channel.
|
||||||
|
backgroundColor: 'transparent',
|
||||||
|
flex: 1,
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
transform: [{translateY: -(insets.top / 2)}, {scale: 0.1}], // scale from 1000px to 100px
|
||||||
|
},
|
||||||
|
]}>
|
||||||
|
<AnimatedLogo
|
||||||
|
fill={isDarkMode ? colors.blue3 : '#fff'}
|
||||||
|
style={[logoAnimations]}
|
||||||
|
/>
|
||||||
|
</Animated.View>
|
||||||
|
}>
|
||||||
|
{!isAnimationComplete && (
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
StyleSheet.absoluteFillObject,
|
||||||
|
{backgroundColor: isDarkMode ? colors.blue3 : '#fff'},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<Animated.View style={[{flex: 1}, appAnimation]}>
|
||||||
|
{props.children}
|
||||||
|
</Animated.View>
|
||||||
|
</MaskedView>
|
||||||
|
))}
|
||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,56 @@
|
|||||||
|
# Application Layout Framework (ALF)
|
||||||
|
|
||||||
|
A set of UI primitives and components.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
Naming conventions follow Tailwind — delimited with a `_` instead of `-` to
|
||||||
|
enable object access — with a couple exceptions:
|
||||||
|
|
||||||
|
**Spacing**
|
||||||
|
|
||||||
|
Uses "t-shirt" sizes `xxs`, `xs`, `sm`, `md`, `lg`, `xl` and `xxl` instead of
|
||||||
|
increments of 4px. We only use a few common spacings, and otherwise typically
|
||||||
|
rely on many one-off values.
|
||||||
|
|
||||||
|
**Text Size**
|
||||||
|
|
||||||
|
Uses "t-shirt" sizes `xxs`, `xs`, `sm`, `md`, `lg`, `xl` and `xxl` to match our
|
||||||
|
type scale.
|
||||||
|
|
||||||
|
**Line Height**
|
||||||
|
|
||||||
|
The text size atoms also apply a line-height with the same value as the size,
|
||||||
|
for a 1:1 ratio. `tight` and `normal` are retained for use in the few places
|
||||||
|
where we need leading.
|
||||||
|
|
||||||
|
### Atoms
|
||||||
|
|
||||||
|
An (mostly-complete) set of style definitions that match Tailwind CSS selectors.
|
||||||
|
These are static and reused throughout the app.
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { atoms } from '#/alf'
|
||||||
|
|
||||||
|
<View style={[atoms.flex_row]} />
|
||||||
|
```
|
||||||
|
|
||||||
|
### Theme
|
||||||
|
|
||||||
|
Any values that rely on the theme, namely colors.
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
const t = useTheme()
|
||||||
|
|
||||||
|
<View style={[atoms.flex_row, t.atoms.bg]} />
|
||||||
|
```
|
||||||
|
|
||||||
|
### Breakpoints
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
const b = useBreakpoints()
|
||||||
|
|
||||||
|
if (b.gtMobile) {
|
||||||
|
// render tablet or desktop UI
|
||||||
|
}
|
||||||
|
```
|
||||||
@@ -0,0 +1,514 @@
|
|||||||
|
import * as tokens from '#/alf/tokens'
|
||||||
|
|
||||||
|
export const atoms = {
|
||||||
|
/*
|
||||||
|
* Positioning
|
||||||
|
*/
|
||||||
|
absolute: {
|
||||||
|
position: 'absolute',
|
||||||
|
},
|
||||||
|
relative: {
|
||||||
|
position: 'relative',
|
||||||
|
},
|
||||||
|
inset_0: {
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
bottom: 0,
|
||||||
|
},
|
||||||
|
z_10: {
|
||||||
|
zIndex: 10,
|
||||||
|
},
|
||||||
|
z_20: {
|
||||||
|
zIndex: 20,
|
||||||
|
},
|
||||||
|
z_30: {
|
||||||
|
zIndex: 30,
|
||||||
|
},
|
||||||
|
z_40: {
|
||||||
|
zIndex: 40,
|
||||||
|
},
|
||||||
|
z_50: {
|
||||||
|
zIndex: 50,
|
||||||
|
},
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Width
|
||||||
|
*/
|
||||||
|
w_full: {
|
||||||
|
width: '100%',
|
||||||
|
},
|
||||||
|
h_full: {
|
||||||
|
height: '100%',
|
||||||
|
},
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Border radius
|
||||||
|
*/
|
||||||
|
rounded_sm: {
|
||||||
|
borderRadius: tokens.borderRadius.sm,
|
||||||
|
},
|
||||||
|
rounded_md: {
|
||||||
|
borderRadius: tokens.borderRadius.md,
|
||||||
|
},
|
||||||
|
rounded_full: {
|
||||||
|
borderRadius: tokens.borderRadius.full,
|
||||||
|
},
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Flex
|
||||||
|
*/
|
||||||
|
gap_xxs: {
|
||||||
|
gap: tokens.space.xxs,
|
||||||
|
},
|
||||||
|
gap_xs: {
|
||||||
|
gap: tokens.space.xs,
|
||||||
|
},
|
||||||
|
gap_sm: {
|
||||||
|
gap: tokens.space.sm,
|
||||||
|
},
|
||||||
|
gap_md: {
|
||||||
|
gap: tokens.space.md,
|
||||||
|
},
|
||||||
|
gap_lg: {
|
||||||
|
gap: tokens.space.lg,
|
||||||
|
},
|
||||||
|
gap_xl: {
|
||||||
|
gap: tokens.space.xl,
|
||||||
|
},
|
||||||
|
gap_xxl: {
|
||||||
|
gap: tokens.space.xxl,
|
||||||
|
},
|
||||||
|
flex: {
|
||||||
|
display: 'flex',
|
||||||
|
},
|
||||||
|
flex_row: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
},
|
||||||
|
flex_wrap: {
|
||||||
|
flexWrap: 'wrap',
|
||||||
|
},
|
||||||
|
flex_1: {
|
||||||
|
flex: 1,
|
||||||
|
},
|
||||||
|
flex_grow: {
|
||||||
|
flexGrow: 1,
|
||||||
|
},
|
||||||
|
flex_shrink: {
|
||||||
|
flexShrink: 1,
|
||||||
|
},
|
||||||
|
justify_center: {
|
||||||
|
justifyContent: 'center',
|
||||||
|
},
|
||||||
|
justify_between: {
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
},
|
||||||
|
justify_end: {
|
||||||
|
justifyContent: 'flex-end',
|
||||||
|
},
|
||||||
|
align_center: {
|
||||||
|
alignItems: 'center',
|
||||||
|
},
|
||||||
|
align_start: {
|
||||||
|
alignItems: 'flex-start',
|
||||||
|
},
|
||||||
|
align_end: {
|
||||||
|
alignItems: 'flex-end',
|
||||||
|
},
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Text
|
||||||
|
*/
|
||||||
|
text_center: {
|
||||||
|
textAlign: 'center',
|
||||||
|
},
|
||||||
|
text_right: {
|
||||||
|
textAlign: 'right',
|
||||||
|
},
|
||||||
|
text_xxs: {
|
||||||
|
fontSize: tokens.fontSize.xxs,
|
||||||
|
lineHeight: tokens.fontSize.xxs,
|
||||||
|
},
|
||||||
|
text_xs: {
|
||||||
|
fontSize: tokens.fontSize.xs,
|
||||||
|
lineHeight: tokens.fontSize.xs,
|
||||||
|
},
|
||||||
|
text_sm: {
|
||||||
|
fontSize: tokens.fontSize.sm,
|
||||||
|
lineHeight: tokens.fontSize.sm,
|
||||||
|
},
|
||||||
|
text_md: {
|
||||||
|
fontSize: tokens.fontSize.md,
|
||||||
|
lineHeight: tokens.fontSize.md,
|
||||||
|
},
|
||||||
|
text_lg: {
|
||||||
|
fontSize: tokens.fontSize.lg,
|
||||||
|
lineHeight: tokens.fontSize.lg,
|
||||||
|
},
|
||||||
|
text_xl: {
|
||||||
|
fontSize: tokens.fontSize.xl,
|
||||||
|
lineHeight: tokens.fontSize.xl,
|
||||||
|
},
|
||||||
|
text_xxl: {
|
||||||
|
fontSize: tokens.fontSize.xxl,
|
||||||
|
lineHeight: tokens.fontSize.xxl,
|
||||||
|
},
|
||||||
|
leading_tight: {
|
||||||
|
lineHeight: 1.25,
|
||||||
|
},
|
||||||
|
leading_normal: {
|
||||||
|
lineHeight: 1.5,
|
||||||
|
},
|
||||||
|
font_normal: {
|
||||||
|
fontWeight: tokens.fontWeight.normal,
|
||||||
|
},
|
||||||
|
font_semibold: {
|
||||||
|
fontWeight: tokens.fontWeight.semibold,
|
||||||
|
},
|
||||||
|
font_bold: {
|
||||||
|
fontWeight: tokens.fontWeight.bold,
|
||||||
|
},
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Border
|
||||||
|
*/
|
||||||
|
border: {
|
||||||
|
borderWidth: 1,
|
||||||
|
},
|
||||||
|
border_t: {
|
||||||
|
borderTopWidth: 1,
|
||||||
|
},
|
||||||
|
border_b: {
|
||||||
|
borderBottomWidth: 1,
|
||||||
|
},
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Padding
|
||||||
|
*/
|
||||||
|
p_xxs: {
|
||||||
|
padding: tokens.space.xxs,
|
||||||
|
},
|
||||||
|
p_xs: {
|
||||||
|
padding: tokens.space.xs,
|
||||||
|
},
|
||||||
|
p_sm: {
|
||||||
|
padding: tokens.space.sm,
|
||||||
|
},
|
||||||
|
p_md: {
|
||||||
|
padding: tokens.space.md,
|
||||||
|
},
|
||||||
|
p_lg: {
|
||||||
|
padding: tokens.space.lg,
|
||||||
|
},
|
||||||
|
p_xl: {
|
||||||
|
padding: tokens.space.xl,
|
||||||
|
},
|
||||||
|
p_xxl: {
|
||||||
|
padding: tokens.space.xxl,
|
||||||
|
},
|
||||||
|
px_xxs: {
|
||||||
|
paddingLeft: tokens.space.xxs,
|
||||||
|
paddingRight: tokens.space.xxs,
|
||||||
|
},
|
||||||
|
px_xs: {
|
||||||
|
paddingLeft: tokens.space.xs,
|
||||||
|
paddingRight: tokens.space.xs,
|
||||||
|
},
|
||||||
|
px_sm: {
|
||||||
|
paddingLeft: tokens.space.sm,
|
||||||
|
paddingRight: tokens.space.sm,
|
||||||
|
},
|
||||||
|
px_md: {
|
||||||
|
paddingLeft: tokens.space.md,
|
||||||
|
paddingRight: tokens.space.md,
|
||||||
|
},
|
||||||
|
px_lg: {
|
||||||
|
paddingLeft: tokens.space.lg,
|
||||||
|
paddingRight: tokens.space.lg,
|
||||||
|
},
|
||||||
|
px_xl: {
|
||||||
|
paddingLeft: tokens.space.xl,
|
||||||
|
paddingRight: tokens.space.xl,
|
||||||
|
},
|
||||||
|
px_xxl: {
|
||||||
|
paddingLeft: tokens.space.xxl,
|
||||||
|
paddingRight: tokens.space.xxl,
|
||||||
|
},
|
||||||
|
py_xxs: {
|
||||||
|
paddingTop: tokens.space.xxs,
|
||||||
|
paddingBottom: tokens.space.xxs,
|
||||||
|
},
|
||||||
|
py_xs: {
|
||||||
|
paddingTop: tokens.space.xs,
|
||||||
|
paddingBottom: tokens.space.xs,
|
||||||
|
},
|
||||||
|
py_sm: {
|
||||||
|
paddingTop: tokens.space.sm,
|
||||||
|
paddingBottom: tokens.space.sm,
|
||||||
|
},
|
||||||
|
py_md: {
|
||||||
|
paddingTop: tokens.space.md,
|
||||||
|
paddingBottom: tokens.space.md,
|
||||||
|
},
|
||||||
|
py_lg: {
|
||||||
|
paddingTop: tokens.space.lg,
|
||||||
|
paddingBottom: tokens.space.lg,
|
||||||
|
},
|
||||||
|
py_xl: {
|
||||||
|
paddingTop: tokens.space.xl,
|
||||||
|
paddingBottom: tokens.space.xl,
|
||||||
|
},
|
||||||
|
py_xxl: {
|
||||||
|
paddingTop: tokens.space.xxl,
|
||||||
|
paddingBottom: tokens.space.xxl,
|
||||||
|
},
|
||||||
|
pt_xxs: {
|
||||||
|
paddingTop: tokens.space.xxs,
|
||||||
|
},
|
||||||
|
pt_xs: {
|
||||||
|
paddingTop: tokens.space.xs,
|
||||||
|
},
|
||||||
|
pt_sm: {
|
||||||
|
paddingTop: tokens.space.sm,
|
||||||
|
},
|
||||||
|
pt_md: {
|
||||||
|
paddingTop: tokens.space.md,
|
||||||
|
},
|
||||||
|
pt_lg: {
|
||||||
|
paddingTop: tokens.space.lg,
|
||||||
|
},
|
||||||
|
pt_xl: {
|
||||||
|
paddingTop: tokens.space.xl,
|
||||||
|
},
|
||||||
|
pt_xxl: {
|
||||||
|
paddingTop: tokens.space.xxl,
|
||||||
|
},
|
||||||
|
pb_xxs: {
|
||||||
|
paddingBottom: tokens.space.xxs,
|
||||||
|
},
|
||||||
|
pb_xs: {
|
||||||
|
paddingBottom: tokens.space.xs,
|
||||||
|
},
|
||||||
|
pb_sm: {
|
||||||
|
paddingBottom: tokens.space.sm,
|
||||||
|
},
|
||||||
|
pb_md: {
|
||||||
|
paddingBottom: tokens.space.md,
|
||||||
|
},
|
||||||
|
pb_lg: {
|
||||||
|
paddingBottom: tokens.space.lg,
|
||||||
|
},
|
||||||
|
pb_xl: {
|
||||||
|
paddingBottom: tokens.space.xl,
|
||||||
|
},
|
||||||
|
pb_xxl: {
|
||||||
|
paddingBottom: tokens.space.xxl,
|
||||||
|
},
|
||||||
|
pl_xxs: {
|
||||||
|
paddingLeft: tokens.space.xxs,
|
||||||
|
},
|
||||||
|
pl_xs: {
|
||||||
|
paddingLeft: tokens.space.xs,
|
||||||
|
},
|
||||||
|
pl_sm: {
|
||||||
|
paddingLeft: tokens.space.sm,
|
||||||
|
},
|
||||||
|
pl_md: {
|
||||||
|
paddingLeft: tokens.space.md,
|
||||||
|
},
|
||||||
|
pl_lg: {
|
||||||
|
paddingLeft: tokens.space.lg,
|
||||||
|
},
|
||||||
|
pl_xl: {
|
||||||
|
paddingLeft: tokens.space.xl,
|
||||||
|
},
|
||||||
|
pl_xxl: {
|
||||||
|
paddingLeft: tokens.space.xxl,
|
||||||
|
},
|
||||||
|
pr_xxs: {
|
||||||
|
paddingRight: tokens.space.xxs,
|
||||||
|
},
|
||||||
|
pr_xs: {
|
||||||
|
paddingRight: tokens.space.xs,
|
||||||
|
},
|
||||||
|
pr_sm: {
|
||||||
|
paddingRight: tokens.space.sm,
|
||||||
|
},
|
||||||
|
pr_md: {
|
||||||
|
paddingRight: tokens.space.md,
|
||||||
|
},
|
||||||
|
pr_lg: {
|
||||||
|
paddingRight: tokens.space.lg,
|
||||||
|
},
|
||||||
|
pr_xl: {
|
||||||
|
paddingRight: tokens.space.xl,
|
||||||
|
},
|
||||||
|
pr_xxl: {
|
||||||
|
paddingRight: tokens.space.xxl,
|
||||||
|
},
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Margin
|
||||||
|
*/
|
||||||
|
m_xxs: {
|
||||||
|
margin: tokens.space.xxs,
|
||||||
|
},
|
||||||
|
m_xs: {
|
||||||
|
margin: tokens.space.xs,
|
||||||
|
},
|
||||||
|
m_sm: {
|
||||||
|
margin: tokens.space.sm,
|
||||||
|
},
|
||||||
|
m_md: {
|
||||||
|
margin: tokens.space.md,
|
||||||
|
},
|
||||||
|
m_lg: {
|
||||||
|
margin: tokens.space.lg,
|
||||||
|
},
|
||||||
|
m_xl: {
|
||||||
|
margin: tokens.space.xl,
|
||||||
|
},
|
||||||
|
m_xxl: {
|
||||||
|
margin: tokens.space.xxl,
|
||||||
|
},
|
||||||
|
mx_xxs: {
|
||||||
|
marginLeft: tokens.space.xxs,
|
||||||
|
marginRight: tokens.space.xxs,
|
||||||
|
},
|
||||||
|
mx_xs: {
|
||||||
|
marginLeft: tokens.space.xs,
|
||||||
|
marginRight: tokens.space.xs,
|
||||||
|
},
|
||||||
|
mx_sm: {
|
||||||
|
marginLeft: tokens.space.sm,
|
||||||
|
marginRight: tokens.space.sm,
|
||||||
|
},
|
||||||
|
mx_md: {
|
||||||
|
marginLeft: tokens.space.md,
|
||||||
|
marginRight: tokens.space.md,
|
||||||
|
},
|
||||||
|
mx_lg: {
|
||||||
|
marginLeft: tokens.space.lg,
|
||||||
|
marginRight: tokens.space.lg,
|
||||||
|
},
|
||||||
|
mx_xl: {
|
||||||
|
marginLeft: tokens.space.xl,
|
||||||
|
marginRight: tokens.space.xl,
|
||||||
|
},
|
||||||
|
mx_xxl: {
|
||||||
|
marginLeft: tokens.space.xxl,
|
||||||
|
marginRight: tokens.space.xxl,
|
||||||
|
},
|
||||||
|
my_xxs: {
|
||||||
|
marginTop: tokens.space.xxs,
|
||||||
|
marginBottom: tokens.space.xxs,
|
||||||
|
},
|
||||||
|
my_xs: {
|
||||||
|
marginTop: tokens.space.xs,
|
||||||
|
marginBottom: tokens.space.xs,
|
||||||
|
},
|
||||||
|
my_sm: {
|
||||||
|
marginTop: tokens.space.sm,
|
||||||
|
marginBottom: tokens.space.sm,
|
||||||
|
},
|
||||||
|
my_md: {
|
||||||
|
marginTop: tokens.space.md,
|
||||||
|
marginBottom: tokens.space.md,
|
||||||
|
},
|
||||||
|
my_lg: {
|
||||||
|
marginTop: tokens.space.lg,
|
||||||
|
marginBottom: tokens.space.lg,
|
||||||
|
},
|
||||||
|
my_xl: {
|
||||||
|
marginTop: tokens.space.xl,
|
||||||
|
marginBottom: tokens.space.xl,
|
||||||
|
},
|
||||||
|
my_xxl: {
|
||||||
|
marginTop: tokens.space.xxl,
|
||||||
|
marginBottom: tokens.space.xxl,
|
||||||
|
},
|
||||||
|
mt_xxs: {
|
||||||
|
marginTop: tokens.space.xxs,
|
||||||
|
},
|
||||||
|
mt_xs: {
|
||||||
|
marginTop: tokens.space.xs,
|
||||||
|
},
|
||||||
|
mt_sm: {
|
||||||
|
marginTop: tokens.space.sm,
|
||||||
|
},
|
||||||
|
mt_md: {
|
||||||
|
marginTop: tokens.space.md,
|
||||||
|
},
|
||||||
|
mt_lg: {
|
||||||
|
marginTop: tokens.space.lg,
|
||||||
|
},
|
||||||
|
mt_xl: {
|
||||||
|
marginTop: tokens.space.xl,
|
||||||
|
},
|
||||||
|
mt_xxl: {
|
||||||
|
marginTop: tokens.space.xxl,
|
||||||
|
},
|
||||||
|
mb_xxs: {
|
||||||
|
marginBottom: tokens.space.xxs,
|
||||||
|
},
|
||||||
|
mb_xs: {
|
||||||
|
marginBottom: tokens.space.xs,
|
||||||
|
},
|
||||||
|
mb_sm: {
|
||||||
|
marginBottom: tokens.space.sm,
|
||||||
|
},
|
||||||
|
mb_md: {
|
||||||
|
marginBottom: tokens.space.md,
|
||||||
|
},
|
||||||
|
mb_lg: {
|
||||||
|
marginBottom: tokens.space.lg,
|
||||||
|
},
|
||||||
|
mb_xl: {
|
||||||
|
marginBottom: tokens.space.xl,
|
||||||
|
},
|
||||||
|
mb_xxl: {
|
||||||
|
marginBottom: tokens.space.xxl,
|
||||||
|
},
|
||||||
|
ml_xxs: {
|
||||||
|
marginLeft: tokens.space.xxs,
|
||||||
|
},
|
||||||
|
ml_xs: {
|
||||||
|
marginLeft: tokens.space.xs,
|
||||||
|
},
|
||||||
|
ml_sm: {
|
||||||
|
marginLeft: tokens.space.sm,
|
||||||
|
},
|
||||||
|
ml_md: {
|
||||||
|
marginLeft: tokens.space.md,
|
||||||
|
},
|
||||||
|
ml_lg: {
|
||||||
|
marginLeft: tokens.space.lg,
|
||||||
|
},
|
||||||
|
ml_xl: {
|
||||||
|
marginLeft: tokens.space.xl,
|
||||||
|
},
|
||||||
|
ml_xxl: {
|
||||||
|
marginLeft: tokens.space.xxl,
|
||||||
|
},
|
||||||
|
mr_xxs: {
|
||||||
|
marginRight: tokens.space.xxs,
|
||||||
|
},
|
||||||
|
mr_xs: {
|
||||||
|
marginRight: tokens.space.xs,
|
||||||
|
},
|
||||||
|
mr_sm: {
|
||||||
|
marginRight: tokens.space.sm,
|
||||||
|
},
|
||||||
|
mr_md: {
|
||||||
|
marginRight: tokens.space.md,
|
||||||
|
},
|
||||||
|
mr_lg: {
|
||||||
|
marginRight: tokens.space.lg,
|
||||||
|
},
|
||||||
|
mr_xl: {
|
||||||
|
marginRight: tokens.space.xl,
|
||||||
|
},
|
||||||
|
mr_xxl: {
|
||||||
|
marginRight: tokens.space.xxl,
|
||||||
|
},
|
||||||
|
} as const
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import {Dimensions} from 'react-native'
|
||||||
|
import * as themes from '#/alf/themes'
|
||||||
|
|
||||||
|
export * as tokens from '#/alf/tokens'
|
||||||
|
export {atoms} from '#/alf/atoms'
|
||||||
|
export * from '#/alf/util/platform'
|
||||||
|
|
||||||
|
type BreakpointName = keyof typeof breakpoints
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Breakpoints
|
||||||
|
*/
|
||||||
|
const breakpoints: {
|
||||||
|
[key: string]: number
|
||||||
|
} = {
|
||||||
|
gtMobile: 800,
|
||||||
|
gtTablet: 1200,
|
||||||
|
}
|
||||||
|
function getActiveBreakpoints({width}: {width: number}) {
|
||||||
|
const active: (keyof typeof breakpoints)[] = Object.keys(breakpoints).filter(
|
||||||
|
breakpoint => width >= breakpoints[breakpoint],
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
active: active[active.length - 1],
|
||||||
|
gtMobile: active.includes('gtMobile'),
|
||||||
|
gtTablet: active.includes('gtTablet'),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Context
|
||||||
|
*/
|
||||||
|
export const Context = React.createContext<{
|
||||||
|
themeName: themes.ThemeName
|
||||||
|
theme: themes.Theme
|
||||||
|
breakpoints: {
|
||||||
|
active: BreakpointName | undefined
|
||||||
|
gtMobile: boolean
|
||||||
|
gtTablet: boolean
|
||||||
|
}
|
||||||
|
}>({
|
||||||
|
themeName: 'light',
|
||||||
|
theme: themes.light,
|
||||||
|
breakpoints: {
|
||||||
|
active: undefined,
|
||||||
|
gtMobile: false,
|
||||||
|
gtTablet: false,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
export function ThemeProvider({
|
||||||
|
children,
|
||||||
|
theme: themeName,
|
||||||
|
}: React.PropsWithChildren<{theme: themes.ThemeName}>) {
|
||||||
|
const theme = themes[themeName]
|
||||||
|
const [breakpoints, setBreakpoints] = React.useState(() =>
|
||||||
|
getActiveBreakpoints({width: Dimensions.get('window').width}),
|
||||||
|
)
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
const listener = Dimensions.addEventListener('change', ({window}) => {
|
||||||
|
const bp = getActiveBreakpoints({width: window.width})
|
||||||
|
if (bp.active !== breakpoints.active) setBreakpoints(bp)
|
||||||
|
})
|
||||||
|
|
||||||
|
return listener.remove
|
||||||
|
}, [breakpoints, setBreakpoints])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Context.Provider
|
||||||
|
value={React.useMemo(
|
||||||
|
() => ({
|
||||||
|
themeName: themeName,
|
||||||
|
theme: theme,
|
||||||
|
breakpoints,
|
||||||
|
}),
|
||||||
|
[theme, themeName, breakpoints],
|
||||||
|
)}>
|
||||||
|
{children}
|
||||||
|
</Context.Provider>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useTheme() {
|
||||||
|
return React.useContext(Context).theme
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useBreakpoints() {
|
||||||
|
return React.useContext(Context).breakpoints
|
||||||
|
}
|
||||||
@@ -0,0 +1,108 @@
|
|||||||
|
import * as tokens from '#/alf/tokens'
|
||||||
|
import type {Mutable} from '#/alf/types'
|
||||||
|
|
||||||
|
export type ThemeName = 'light' | 'dark'
|
||||||
|
export type ReadonlyTheme = typeof light
|
||||||
|
export type Theme = Mutable<ReadonlyTheme>
|
||||||
|
|
||||||
|
export type Palette = {
|
||||||
|
primary: string
|
||||||
|
positive: string
|
||||||
|
negative: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const lightPalette: Palette = {
|
||||||
|
primary: tokens.color.blue_500,
|
||||||
|
positive: tokens.color.green_500,
|
||||||
|
negative: tokens.color.red_500,
|
||||||
|
} as const
|
||||||
|
|
||||||
|
export const darkPalette: Palette = {
|
||||||
|
primary: tokens.color.blue_500,
|
||||||
|
positive: tokens.color.green_400,
|
||||||
|
negative: tokens.color.red_400,
|
||||||
|
} as const
|
||||||
|
|
||||||
|
export const light = {
|
||||||
|
palette: lightPalette,
|
||||||
|
atoms: {
|
||||||
|
text: {
|
||||||
|
color: tokens.color.gray_1000,
|
||||||
|
},
|
||||||
|
text_contrast_700: {
|
||||||
|
color: tokens.color.gray_700,
|
||||||
|
},
|
||||||
|
text_contrast_500: {
|
||||||
|
color: tokens.color.gray_500,
|
||||||
|
},
|
||||||
|
text_inverted: {
|
||||||
|
color: tokens.color.white,
|
||||||
|
},
|
||||||
|
bg: {
|
||||||
|
backgroundColor: tokens.color.white,
|
||||||
|
},
|
||||||
|
bg_contrast_100: {
|
||||||
|
backgroundColor: tokens.color.gray_100,
|
||||||
|
},
|
||||||
|
bg_contrast_200: {
|
||||||
|
backgroundColor: tokens.color.gray_200,
|
||||||
|
},
|
||||||
|
bg_contrast_300: {
|
||||||
|
backgroundColor: tokens.color.gray_300,
|
||||||
|
},
|
||||||
|
bg_positive: {
|
||||||
|
backgroundColor: tokens.color.green_500,
|
||||||
|
},
|
||||||
|
bg_negative: {
|
||||||
|
backgroundColor: tokens.color.red_400,
|
||||||
|
},
|
||||||
|
border: {
|
||||||
|
borderColor: tokens.color.gray_200,
|
||||||
|
},
|
||||||
|
border_contrast_500: {
|
||||||
|
borderColor: tokens.color.gray_500,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const dark: Theme = {
|
||||||
|
palette: darkPalette,
|
||||||
|
atoms: {
|
||||||
|
text: {
|
||||||
|
color: tokens.color.white,
|
||||||
|
},
|
||||||
|
text_contrast_700: {
|
||||||
|
color: tokens.color.gray_300,
|
||||||
|
},
|
||||||
|
text_contrast_500: {
|
||||||
|
color: tokens.color.gray_500,
|
||||||
|
},
|
||||||
|
text_inverted: {
|
||||||
|
color: tokens.color.gray_1000,
|
||||||
|
},
|
||||||
|
bg: {
|
||||||
|
backgroundColor: tokens.color.gray_1000,
|
||||||
|
},
|
||||||
|
bg_contrast_100: {
|
||||||
|
backgroundColor: tokens.color.gray_900,
|
||||||
|
},
|
||||||
|
bg_contrast_200: {
|
||||||
|
backgroundColor: tokens.color.gray_800,
|
||||||
|
},
|
||||||
|
bg_contrast_300: {
|
||||||
|
backgroundColor: tokens.color.gray_700,
|
||||||
|
},
|
||||||
|
bg_positive: {
|
||||||
|
backgroundColor: tokens.color.green_400,
|
||||||
|
},
|
||||||
|
bg_negative: {
|
||||||
|
backgroundColor: tokens.color.red_400,
|
||||||
|
},
|
||||||
|
border: {
|
||||||
|
borderColor: tokens.color.gray_800,
|
||||||
|
},
|
||||||
|
border_contrast_500: {
|
||||||
|
borderColor: tokens.color.gray_500,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
const BLUE_HUE = 211
|
||||||
|
const GRAYSCALE_SATURATION = 22
|
||||||
|
|
||||||
|
export const color = {
|
||||||
|
white: '#FFFFFF',
|
||||||
|
|
||||||
|
gray_0: `hsl(${BLUE_HUE}, ${GRAYSCALE_SATURATION}%, 100%)`,
|
||||||
|
gray_100: `hsl(${BLUE_HUE}, ${GRAYSCALE_SATURATION}%, 95%)`,
|
||||||
|
gray_200: `hsl(${BLUE_HUE}, ${GRAYSCALE_SATURATION}%, 85%)`,
|
||||||
|
gray_300: `hsl(${BLUE_HUE}, ${GRAYSCALE_SATURATION}%, 75%)`,
|
||||||
|
gray_400: `hsl(${BLUE_HUE}, ${GRAYSCALE_SATURATION}%, 65%)`,
|
||||||
|
gray_500: `hsl(${BLUE_HUE}, ${GRAYSCALE_SATURATION}%, 55%)`,
|
||||||
|
gray_600: `hsl(${BLUE_HUE}, ${GRAYSCALE_SATURATION}%, 45%)`,
|
||||||
|
gray_700: `hsl(${BLUE_HUE}, ${GRAYSCALE_SATURATION}%, 35%)`,
|
||||||
|
gray_800: `hsl(${BLUE_HUE}, ${GRAYSCALE_SATURATION}%, 25%)`,
|
||||||
|
gray_900: `hsl(${BLUE_HUE}, ${GRAYSCALE_SATURATION}%, 15%)`,
|
||||||
|
gray_1000: `hsl(${BLUE_HUE}, ${GRAYSCALE_SATURATION}%, 5%)`,
|
||||||
|
|
||||||
|
blue_0: `hsl(${BLUE_HUE}, 99%, 100%)`,
|
||||||
|
blue_100: `hsl(${BLUE_HUE}, 99%, 93%)`,
|
||||||
|
blue_200: `hsl(${BLUE_HUE}, 99%, 83%)`,
|
||||||
|
blue_300: `hsl(${BLUE_HUE}, 99%, 73%)`,
|
||||||
|
blue_400: `hsl(${BLUE_HUE}, 99%, 63%)`,
|
||||||
|
blue_500: `hsl(${BLUE_HUE}, 99%, 53%)`,
|
||||||
|
blue_600: `hsl(${BLUE_HUE}, 99%, 43%)`,
|
||||||
|
blue_700: `hsl(${BLUE_HUE}, 99%, 33%)`,
|
||||||
|
blue_800: `hsl(${BLUE_HUE}, 99%, 23%)`,
|
||||||
|
blue_900: `hsl(${BLUE_HUE}, 99%, 13%)`,
|
||||||
|
blue_1000: `hsl(${BLUE_HUE}, 99%, 8%)`,
|
||||||
|
|
||||||
|
green_0: `hsl(130, 60%, 100%)`,
|
||||||
|
green_100: `hsl(130, 60%, 95%)`,
|
||||||
|
green_200: `hsl(130, 60%, 85%)`,
|
||||||
|
green_300: `hsl(130, 60%, 75%)`,
|
||||||
|
green_400: `hsl(130, 60%, 65%)`,
|
||||||
|
green_500: `hsl(130, 60%, 55%)`,
|
||||||
|
green_600: `hsl(130, 60%, 45%)`,
|
||||||
|
green_700: `hsl(130, 60%, 35%)`,
|
||||||
|
green_800: `hsl(130, 60%, 25%)`,
|
||||||
|
green_900: `hsl(130, 60%, 15%)`,
|
||||||
|
green_1000: `hsl(130, 60%, 5%)`,
|
||||||
|
|
||||||
|
red_0: `hsl(349, 96%, 100%)`,
|
||||||
|
red_100: `hsl(349, 96%, 95%)`,
|
||||||
|
red_200: `hsl(349, 96%, 85%)`,
|
||||||
|
red_300: `hsl(349, 96%, 75%)`,
|
||||||
|
red_400: `hsl(349, 96%, 65%)`,
|
||||||
|
red_500: `hsl(349, 96%, 55%)`,
|
||||||
|
red_600: `hsl(349, 96%, 45%)`,
|
||||||
|
red_700: `hsl(349, 96%, 35%)`,
|
||||||
|
red_800: `hsl(349, 96%, 25%)`,
|
||||||
|
red_900: `hsl(349, 96%, 15%)`,
|
||||||
|
red_1000: `hsl(349, 96%, 5%)`,
|
||||||
|
} as const
|
||||||
|
|
||||||
|
export const space = {
|
||||||
|
xxs: 2,
|
||||||
|
xs: 4,
|
||||||
|
sm: 8,
|
||||||
|
md: 12,
|
||||||
|
lg: 18,
|
||||||
|
xl: 24,
|
||||||
|
xxl: 32,
|
||||||
|
} as const
|
||||||
|
|
||||||
|
export const fontSize = {
|
||||||
|
xxs: 10,
|
||||||
|
xs: 12,
|
||||||
|
sm: 14,
|
||||||
|
md: 16,
|
||||||
|
lg: 18,
|
||||||
|
xl: 22,
|
||||||
|
xxl: 26,
|
||||||
|
} as const
|
||||||
|
|
||||||
|
// TODO test
|
||||||
|
export const lineHeight = {
|
||||||
|
none: 1,
|
||||||
|
normal: 1.5,
|
||||||
|
relaxed: 1.625,
|
||||||
|
} as const
|
||||||
|
|
||||||
|
export const borderRadius = {
|
||||||
|
sm: 8,
|
||||||
|
md: 12,
|
||||||
|
full: 999,
|
||||||
|
} as const
|
||||||
|
|
||||||
|
export const fontWeight = {
|
||||||
|
normal: '400',
|
||||||
|
semibold: '600',
|
||||||
|
bold: '900',
|
||||||
|
} as const
|
||||||
|
|
||||||
|
export type Color = keyof typeof color
|
||||||
|
export type Space = keyof typeof space
|
||||||
|
export type FontSize = keyof typeof fontSize
|
||||||
|
export type LineHeight = keyof typeof lineHeight
|
||||||
|
export type BorderRadius = keyof typeof borderRadius
|
||||||
|
export type FontWeight = keyof typeof fontWeight
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
type LiteralToCommon<T extends PropertyKey> = T extends number
|
||||||
|
? number
|
||||||
|
: T extends string
|
||||||
|
? string
|
||||||
|
: T extends symbol
|
||||||
|
? symbol
|
||||||
|
: never
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @see https://stackoverflow.com/questions/68249999/use-as-const-in-typescript-without-adding-readonly-modifiers
|
||||||
|
*/
|
||||||
|
export type Mutable<T> = {
|
||||||
|
-readonly [K in keyof T]: T[K] extends PropertyKey
|
||||||
|
? LiteralToCommon<T[K]>
|
||||||
|
: Mutable<T[K]>
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import {Platform} from 'react-native'
|
||||||
|
|
||||||
|
export function web(value: any) {
|
||||||
|
return Platform.select({
|
||||||
|
web: value,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ios(value: any) {
|
||||||
|
return Platform.select({
|
||||||
|
ios: value,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function android(value: any) {
|
||||||
|
return Platform.select({
|
||||||
|
android: value,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function native(value: any) {
|
||||||
|
return Platform.select({
|
||||||
|
native: value,
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import {useColorScheme} from 'react-native'
|
||||||
|
|
||||||
|
import * as persisted from '#/state/persisted'
|
||||||
|
|
||||||
|
export function useColorModeTheme(
|
||||||
|
theme: persisted.Schema['colorMode'],
|
||||||
|
): 'light' | 'dark' {
|
||||||
|
const colorScheme = useColorScheme()
|
||||||
|
return (theme === 'system' ? colorScheme : theme) || 'light'
|
||||||
|
}
|
||||||
@@ -1,28 +1,17 @@
|
|||||||
import React, {createContext, useContext, useMemo} from 'react'
|
import React, {createContext, useContext, useMemo} from 'react'
|
||||||
import {ScrollHandler} from 'react-native-reanimated'
|
import {ScrollHandlers} from 'react-native-reanimated'
|
||||||
import {NativeScrollEvent} from 'react-native'
|
|
||||||
|
|
||||||
type ScrollHandlers = {
|
const ScrollContext = createContext<ScrollHandlers<any>>({
|
||||||
onBeginDrag: undefined | ScrollHandler
|
|
||||||
onEndDrag: undefined | ScrollHandler<any>
|
|
||||||
onScroll: undefined | ScrollHandler<any>
|
|
||||||
onScrollEndWeb:
|
|
||||||
| undefined
|
|
||||||
| ((e: Pick<NativeScrollEvent, 'contentOffset'>) => void) // Web-only.
|
|
||||||
}
|
|
||||||
|
|
||||||
const ScrollContext = createContext<ScrollHandlers>({
|
|
||||||
onBeginDrag: undefined,
|
onBeginDrag: undefined,
|
||||||
onEndDrag: undefined,
|
onEndDrag: undefined,
|
||||||
onScroll: undefined,
|
onScroll: undefined,
|
||||||
onScrollEndWeb: undefined,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
export function useScrollHandlers(): ScrollHandlers {
|
export function useScrollHandlers(): ScrollHandlers<any> {
|
||||||
return useContext(ScrollContext)
|
return useContext(ScrollContext)
|
||||||
}
|
}
|
||||||
|
|
||||||
type ProviderProps = {children: React.ReactNode} & Partial<ScrollHandlers>
|
type ProviderProps = {children: React.ReactNode} & ScrollHandlers<any>
|
||||||
|
|
||||||
// Note: this completely *overrides* the parent handlers.
|
// Note: this completely *overrides* the parent handlers.
|
||||||
// It's up to you to compose them with the parent ones via useScrollHandlers() if needed.
|
// It's up to you to compose them with the parent ones via useScrollHandlers() if needed.
|
||||||
@@ -31,16 +20,14 @@ export function ScrollProvider({
|
|||||||
onBeginDrag,
|
onBeginDrag,
|
||||||
onEndDrag,
|
onEndDrag,
|
||||||
onScroll,
|
onScroll,
|
||||||
onScrollEndWeb,
|
|
||||||
}: ProviderProps) {
|
}: ProviderProps) {
|
||||||
const handlers = useMemo(
|
const handlers = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
onBeginDrag,
|
onBeginDrag,
|
||||||
onEndDrag,
|
onEndDrag,
|
||||||
onScroll,
|
onScroll,
|
||||||
onScrollEndWeb,
|
|
||||||
}),
|
}),
|
||||||
[onBeginDrag, onEndDrag, onScroll, onScrollEndWeb],
|
[onBeginDrag, onEndDrag, onScroll],
|
||||||
)
|
)
|
||||||
return (
|
return (
|
||||||
<ScrollContext.Provider value={handlers}>{children}</ScrollContext.Provider>
|
<ScrollContext.Provider value={handlers}>{children}</ScrollContext.Provider>
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
import {isWeb} from 'platform/detection'
|
|
||||||
import React, {ReactNode, createContext, useContext} from 'react'
|
import React, {ReactNode, createContext, useContext} from 'react'
|
||||||
import {
|
import {
|
||||||
AppState,
|
|
||||||
TextStyle,
|
TextStyle,
|
||||||
useColorScheme as useColorScheme_BUGGY,
|
useColorScheme,
|
||||||
ViewStyle,
|
ViewStyle,
|
||||||
ColorSchemeName,
|
ColorSchemeName,
|
||||||
} from 'react-native'
|
} from 'react-native'
|
||||||
@@ -97,37 +95,11 @@ function getTheme(theme: ColorSchemeName) {
|
|||||||
return theme === 'dark' ? darkTheme : defaultTheme
|
return theme === 'dark' ? darkTheme : defaultTheme
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* With RN iOS, we can only "trust" the color scheme reported while the app is
|
|
||||||
* active. This is a workaround until the bug is fixed upstream.
|
|
||||||
*
|
|
||||||
* @see https://github.com/bluesky-social/social-app/pull/1417#issuecomment-1719868504
|
|
||||||
* @see https://github.com/facebook/react-native/pull/39439
|
|
||||||
*/
|
|
||||||
function useColorScheme_FIXED() {
|
|
||||||
const colorScheme = useColorScheme_BUGGY()
|
|
||||||
const [currentColorScheme, setCurrentColorScheme] =
|
|
||||||
React.useState<ColorSchemeName>(colorScheme)
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
|
||||||
// we don't need to be updating state on web
|
|
||||||
if (isWeb) return
|
|
||||||
const subscription = AppState.addEventListener('change', state => {
|
|
||||||
const isActive = state === 'active'
|
|
||||||
if (!isActive) return
|
|
||||||
setCurrentColorScheme(colorScheme)
|
|
||||||
})
|
|
||||||
return () => subscription.remove()
|
|
||||||
}, [colorScheme])
|
|
||||||
|
|
||||||
return isWeb ? colorScheme : currentColorScheme
|
|
||||||
}
|
|
||||||
|
|
||||||
export const ThemeProvider: React.FC<ThemeProviderProps> = ({
|
export const ThemeProvider: React.FC<ThemeProviderProps> = ({
|
||||||
theme,
|
theme,
|
||||||
children,
|
children,
|
||||||
}) => {
|
}) => {
|
||||||
const colorScheme = useColorScheme_FIXED()
|
const colorScheme = useColorScheme()
|
||||||
const themeValue = getTheme(theme === 'system' ? colorScheme : theme)
|
const themeValue = getTheme(theme === 'system' ? colorScheme : theme)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -147,6 +147,7 @@ interface ScreenPropertiesMap {
|
|||||||
Settings: {}
|
Settings: {}
|
||||||
AppPasswords: {}
|
AppPasswords: {}
|
||||||
Moderation: {}
|
Moderation: {}
|
||||||
|
PreferencesExternalEmbeds: {}
|
||||||
BlockedAccounts: {}
|
BlockedAccounts: {}
|
||||||
MutedAccounts: {}
|
MutedAccounts: {}
|
||||||
SavedFeeds: {}
|
SavedFeeds: {}
|
||||||
|
|||||||
@@ -117,11 +117,7 @@ export class FeedViewPostsSlice {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class NoopFeedTuner {
|
export class NoopFeedTuner {
|
||||||
private keyCounter = 0
|
reset() {}
|
||||||
|
|
||||||
reset() {
|
|
||||||
this.keyCounter = 0
|
|
||||||
}
|
|
||||||
tune(
|
tune(
|
||||||
feed: FeedViewPost[],
|
feed: FeedViewPost[],
|
||||||
_opts?: {dryRun: boolean; maintainOrder: boolean},
|
_opts?: {dryRun: boolean; maintainOrder: boolean},
|
||||||
@@ -131,13 +127,13 @@ export class NoopFeedTuner {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class FeedTuner {
|
export class FeedTuner {
|
||||||
private keyCounter = 0
|
seenKeys: Set<string> = new Set()
|
||||||
seenUris: Set<string> = new Set()
|
seenUris: Set<string> = new Set()
|
||||||
|
|
||||||
constructor(public tunerFns: FeedTunerFn[]) {}
|
constructor(public tunerFns: FeedTunerFn[]) {}
|
||||||
|
|
||||||
reset() {
|
reset() {
|
||||||
this.keyCounter = 0
|
this.seenKeys.clear()
|
||||||
this.seenUris.clear()
|
this.seenUris.clear()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -218,11 +214,16 @@ export class FeedTuner {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!dryRun) {
|
if (!dryRun) {
|
||||||
for (const slice of slices) {
|
slices = slices.filter(slice => {
|
||||||
|
if (this.seenKeys.has(slice._reactKey)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
for (const item of slice.items) {
|
for (const item of slice.items) {
|
||||||
this.seenUris.add(item.post.uri)
|
this.seenUris.add(item.post.uri)
|
||||||
}
|
}
|
||||||
}
|
this.seenKeys.add(slice._reactKey)
|
||||||
|
return true
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
return slices
|
return slices
|
||||||
|
|||||||
@@ -98,7 +98,7 @@ export class MergeFeedAPI implements FeedAPI {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
cursor: posts.length ? String(this.itemCursor) : undefined,
|
cursor: String(this.itemCursor),
|
||||||
feed: posts,
|
feed: posts,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,69 +0,0 @@
|
|||||||
/**
|
|
||||||
* This is a temporary off-spec search endpoint
|
|
||||||
* TODO removeme when we land this in proto!
|
|
||||||
*/
|
|
||||||
import {AppBskyFeedPost} from '@atproto/api'
|
|
||||||
|
|
||||||
const PROFILES_ENDPOINT = 'https://search.bsky.social/search/profiles'
|
|
||||||
const POSTS_ENDPOINT = 'https://search.bsky.social/search/posts'
|
|
||||||
|
|
||||||
export interface ProfileSearchItem {
|
|
||||||
$type: string
|
|
||||||
avatar: {
|
|
||||||
cid: string
|
|
||||||
mimeType: string
|
|
||||||
}
|
|
||||||
banner: {
|
|
||||||
cid: string
|
|
||||||
mimeType: string
|
|
||||||
}
|
|
||||||
description: string | undefined
|
|
||||||
displayName: string | undefined
|
|
||||||
did: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface PostSearchItem {
|
|
||||||
tid: string
|
|
||||||
cid: string
|
|
||||||
user: {
|
|
||||||
did: string
|
|
||||||
handle: string
|
|
||||||
}
|
|
||||||
post: AppBskyFeedPost.Record
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function searchProfiles(
|
|
||||||
query: string,
|
|
||||||
): Promise<ProfileSearchItem[]> {
|
|
||||||
return await doFetch<ProfileSearchItem[]>(PROFILES_ENDPOINT, query)
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function searchPosts(query: string): Promise<PostSearchItem[]> {
|
|
||||||
return await doFetch<PostSearchItem[]>(POSTS_ENDPOINT, query)
|
|
||||||
}
|
|
||||||
|
|
||||||
async function doFetch<T>(endpoint: string, query: string): Promise<T> {
|
|
||||||
const controller = new AbortController()
|
|
||||||
const to = setTimeout(() => controller.abort(), 15e3)
|
|
||||||
|
|
||||||
const uri = new URL(endpoint)
|
|
||||||
uri.searchParams.set('q', query)
|
|
||||||
|
|
||||||
const res = await fetch(String(uri), {
|
|
||||||
method: 'get',
|
|
||||||
headers: {
|
|
||||||
accept: 'application/json',
|
|
||||||
},
|
|
||||||
signal: controller.signal,
|
|
||||||
})
|
|
||||||
|
|
||||||
const resHeaders: Record<string, string> = {}
|
|
||||||
res.headers.forEach((value: string, key: string) => {
|
|
||||||
resHeaders[key] = value
|
|
||||||
})
|
|
||||||
let resBody = await res.json()
|
|
||||||
|
|
||||||
clearTimeout(to)
|
|
||||||
|
|
||||||
return resBody as unknown as T
|
|
||||||
}
|
|
||||||
@@ -41,7 +41,7 @@ export function IS_LOCAL_DEV(url: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function IS_STAGING(url: string) {
|
export function IS_STAGING(url: string) {
|
||||||
return !IS_LOCAL_DEV(url) && !IS_PROD(url)
|
return url.startsWith('https://staging.bsky.dev')
|
||||||
}
|
}
|
||||||
|
|
||||||
export function IS_PROD(url: string) {
|
export function IS_PROD(url: string) {
|
||||||
@@ -51,7 +51,8 @@ export function IS_PROD(url: string) {
|
|||||||
// -prf
|
// -prf
|
||||||
return (
|
return (
|
||||||
url.startsWith('https://bsky.social') ||
|
url.startsWith('https://bsky.social') ||
|
||||||
url.startsWith('https://api.bsky.app')
|
url.startsWith('https://api.bsky.app') ||
|
||||||
|
/bsky\.network\/?$/.test(url)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -116,8 +117,8 @@ export async function DEFAULT_FEEDS(
|
|||||||
} else {
|
} else {
|
||||||
// production
|
// production
|
||||||
return {
|
return {
|
||||||
pinned: [],
|
pinned: [PROD_DEFAULT_FEED('whats-hot')],
|
||||||
saved: [],
|
saved: [PROD_DEFAULT_FEED('whats-hot')],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
export function usePhotoLibraryPermission() {
|
||||||
|
const requestPhotoAccessIfNeeded = async () => {
|
||||||
|
// On the, we use <input type="file"> to produce a filepicker
|
||||||
|
// This does not need any permission granting.
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return {requestPhotoAccessIfNeeded}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useCameraPermission() {
|
||||||
|
const requestCameraAccessIfNeeded = async () => {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return {requestCameraAccessIfNeeded}
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@ import {BskyAgent} from '@atproto/api'
|
|||||||
import {isBskyAppUrl} from '../strings/url-helpers'
|
import {isBskyAppUrl} from '../strings/url-helpers'
|
||||||
import {extractBskyMeta} from './bsky'
|
import {extractBskyMeta} from './bsky'
|
||||||
import {LINK_META_PROXY} from 'lib/constants'
|
import {LINK_META_PROXY} from 'lib/constants'
|
||||||
|
import {getGiphyMetaUri} from 'lib/strings/embed-player'
|
||||||
|
|
||||||
export enum LikelyType {
|
export enum LikelyType {
|
||||||
HTML,
|
HTML,
|
||||||
@@ -34,6 +35,13 @@ export async function getLinkMeta(
|
|||||||
let urlp
|
let urlp
|
||||||
try {
|
try {
|
||||||
urlp = new URL(url)
|
urlp = new URL(url)
|
||||||
|
|
||||||
|
// Get Giphy meta uri if this is any form of giphy link
|
||||||
|
const giphyMetaUri = getGiphyMetaUri(urlp)
|
||||||
|
if (giphyMetaUri) {
|
||||||
|
url = giphyMetaUri
|
||||||
|
urlp = new URL(url)
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return {
|
return {
|
||||||
error: 'Invalid URL',
|
error: 'Invalid URL',
|
||||||
|
|||||||
@@ -117,9 +117,6 @@ function createResizedImage(
|
|||||||
return reject(new Error('Failed to resize image'))
|
return reject(new Error('Failed to resize image'))
|
||||||
}
|
}
|
||||||
|
|
||||||
canvas.width = width
|
|
||||||
canvas.height = height
|
|
||||||
|
|
||||||
let scale = 1
|
let scale = 1
|
||||||
if (mode === 'cover') {
|
if (mode === 'cover') {
|
||||||
scale = img.width < img.height ? width / img.width : height / img.height
|
scale = img.width < img.height ? width / img.width : height / img.height
|
||||||
@@ -128,10 +125,11 @@ function createResizedImage(
|
|||||||
}
|
}
|
||||||
let w = img.width * scale
|
let w = img.width * scale
|
||||||
let h = img.height * scale
|
let h = img.height * scale
|
||||||
let x = (width - w) / 2
|
|
||||||
let y = (height - h) / 2
|
|
||||||
|
|
||||||
ctx.drawImage(img, x, y, w, h)
|
canvas.width = w
|
||||||
|
canvas.height = h
|
||||||
|
|
||||||
|
ctx.drawImage(img, 0, 0, w, h)
|
||||||
resolve(canvas.toDataURL('image/jpeg', quality))
|
resolve(canvas.toDataURL('image/jpeg', quality))
|
||||||
})
|
})
|
||||||
img.src = dataUri
|
img.src = dataUri
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import {
|
|||||||
MediaTypeOptions,
|
MediaTypeOptions,
|
||||||
} from 'expo-image-picker'
|
} from 'expo-image-picker'
|
||||||
import {getDataUriSize} from './util'
|
import {getDataUriSize} from './util'
|
||||||
|
import * as Toast from 'view/com/util/Toast'
|
||||||
|
|
||||||
export async function openPicker(opts?: ImagePickerOptions) {
|
export async function openPicker(opts?: ImagePickerOptions) {
|
||||||
const response = await launchImageLibraryAsync({
|
const response = await launchImageLibraryAsync({
|
||||||
@@ -13,7 +14,11 @@ export async function openPicker(opts?: ImagePickerOptions) {
|
|||||||
...opts,
|
...opts,
|
||||||
})
|
})
|
||||||
|
|
||||||
return (response.assets ?? []).map(image => ({
|
if (response.assets && response.assets.length > 4) {
|
||||||
|
Toast.show('You may only select up to 4 images')
|
||||||
|
}
|
||||||
|
|
||||||
|
return (response.assets ?? []).slice(0, 4).map(image => ({
|
||||||
mime: 'image/jpeg',
|
mime: 'image/jpeg',
|
||||||
height: image.height,
|
height: image.height,
|
||||||
width: image.width,
|
width: image.width,
|
||||||
|
|||||||
@@ -0,0 +1,58 @@
|
|||||||
|
import {
|
||||||
|
AppBskyEmbedRecord,
|
||||||
|
AppBskyEmbedRecordWithMedia,
|
||||||
|
moderatePost,
|
||||||
|
} from '@atproto/api'
|
||||||
|
|
||||||
|
type ModeratePost = typeof moderatePost
|
||||||
|
type Options = Parameters<ModeratePost>[1] & {
|
||||||
|
hiddenPosts?: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export function moderatePost_wrapped(
|
||||||
|
subject: Parameters<ModeratePost>[0],
|
||||||
|
opts: Options,
|
||||||
|
) {
|
||||||
|
const {hiddenPosts = [], ...options} = opts
|
||||||
|
const moderations = moderatePost(subject, options)
|
||||||
|
|
||||||
|
if (hiddenPosts.includes(subject.uri)) {
|
||||||
|
moderations.content.filter = true
|
||||||
|
moderations.content.blur = true
|
||||||
|
if (!moderations.content.cause) {
|
||||||
|
moderations.content.cause = {
|
||||||
|
// @ts-ignore Temporary extension to the moderation system -prf
|
||||||
|
type: 'post-hidden',
|
||||||
|
source: {type: 'user'},
|
||||||
|
priority: 1,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (subject.embed) {
|
||||||
|
let embedHidden = false
|
||||||
|
if (AppBskyEmbedRecord.isViewRecord(subject.embed.record)) {
|
||||||
|
embedHidden = hiddenPosts.includes(subject.embed.record.uri)
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
AppBskyEmbedRecordWithMedia.isView(subject.embed) &&
|
||||||
|
AppBskyEmbedRecord.isViewRecord(subject.embed.record.record)
|
||||||
|
) {
|
||||||
|
embedHidden = hiddenPosts.includes(subject.embed.record.record.uri)
|
||||||
|
}
|
||||||
|
if (embedHidden) {
|
||||||
|
moderations.embed.filter = true
|
||||||
|
moderations.embed.blur = true
|
||||||
|
if (!moderations.embed.cause) {
|
||||||
|
moderations.embed.cause = {
|
||||||
|
// @ts-ignore Temporary extension to the moderation system -prf
|
||||||
|
type: 'post-hidden',
|
||||||
|
source: {type: 'user'},
|
||||||
|
priority: 1,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return moderations
|
||||||
|
}
|
||||||
@@ -60,6 +60,13 @@ export function describeModerationCause(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// @ts-ignore Temporary extension to the moderation system -prf
|
||||||
|
if (cause.type === 'post-hidden') {
|
||||||
|
return {
|
||||||
|
name: 'Post Hidden by You',
|
||||||
|
description: 'You have hidden this post',
|
||||||
|
}
|
||||||
|
}
|
||||||
return cause.labelDef.strings[context].en
|
return cause.labelDef.strings[context].en
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,39 @@
|
|||||||
import {QueryClient} from '@tanstack/react-query'
|
import {AppState, AppStateStatus} from 'react-native'
|
||||||
|
import {QueryClient, focusManager} from '@tanstack/react-query'
|
||||||
|
import {isNative} from '#/platform/detection'
|
||||||
|
|
||||||
|
focusManager.setEventListener(onFocus => {
|
||||||
|
if (isNative) {
|
||||||
|
const subscription = AppState.addEventListener(
|
||||||
|
'change',
|
||||||
|
(status: AppStateStatus) => {
|
||||||
|
focusManager.setFocused(status === 'active')
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
return () => subscription.remove()
|
||||||
|
} else if (typeof window !== 'undefined' && window.addEventListener) {
|
||||||
|
// these handlers are a bit redundant but focus catches when the browser window
|
||||||
|
// is blurred/focused while visibilitychange seems to only handle when the
|
||||||
|
// window minimizes (both of them catch tab changes)
|
||||||
|
// there's no harm to redundant fires because refetchOnWindowFocus is only
|
||||||
|
// used with queries that employ stale data times
|
||||||
|
const handler = () => onFocus()
|
||||||
|
window.addEventListener('focus', handler, false)
|
||||||
|
window.addEventListener('visibilitychange', handler, false)
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('visibilitychange', handler)
|
||||||
|
window.removeEventListener('focus', handler)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
export const queryClient = new QueryClient({
|
export const queryClient = new QueryClient({
|
||||||
defaultOptions: {
|
defaultOptions: {
|
||||||
queries: {
|
queries: {
|
||||||
// NOTE
|
// NOTE
|
||||||
// refetchOnWindowFocus breaks some UIs (like feeds)
|
// refetchOnWindowFocus breaks some UIs (like feeds)
|
||||||
// so we NEVER want to enable this
|
// so we only selectively want to enable this
|
||||||
// -prf
|
// -prf
|
||||||
refetchOnWindowFocus: false,
|
refetchOnWindowFocus: false,
|
||||||
// Structural sharing between responses makes it impossible to rely on
|
// Structural sharing between responses makes it impossible to rely on
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ export type CommonNavigatorParams = {
|
|||||||
SavedFeeds: undefined
|
SavedFeeds: undefined
|
||||||
PreferencesHomeFeed: undefined
|
PreferencesHomeFeed: undefined
|
||||||
PreferencesThreads: undefined
|
PreferencesThreads: undefined
|
||||||
|
PreferencesExternalEmbeds: undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
export type BottomTabNavigatorParams = CommonNavigatorParams & {
|
export type BottomTabNavigatorParams = CommonNavigatorParams & {
|
||||||
|
|||||||
@@ -1,15 +1,59 @@
|
|||||||
export type EmbedPlayerParams =
|
import {Dimensions, Platform} from 'react-native'
|
||||||
| {type: 'youtube_video'; videoId: string; playerUri: string}
|
const {height: SCREEN_HEIGHT} = Dimensions.get('window')
|
||||||
| {type: 'twitch_live'; channelId: string; playerUri: string}
|
|
||||||
| {type: 'spotify_album'; albumId: string; playerUri: string}
|
export const embedPlayerSources = [
|
||||||
| {
|
'youtube',
|
||||||
type: 'spotify_playlist'
|
'youtubeShorts',
|
||||||
playlistId: string
|
'twitch',
|
||||||
playerUri: string
|
'spotify',
|
||||||
}
|
'soundcloud',
|
||||||
| {type: 'spotify_song'; songId: string; playerUri: string}
|
'appleMusic',
|
||||||
| {type: 'soundcloud_track'; user: string; track: string; playerUri: string}
|
'vimeo',
|
||||||
| {type: 'soundcloud_set'; user: string; set: string; playerUri: string}
|
'giphy',
|
||||||
|
'tenor',
|
||||||
|
] as const
|
||||||
|
|
||||||
|
export type EmbedPlayerSource = (typeof embedPlayerSources)[number]
|
||||||
|
|
||||||
|
export type EmbedPlayerType =
|
||||||
|
| 'youtube_video'
|
||||||
|
| 'youtube_short'
|
||||||
|
| 'twitch_video'
|
||||||
|
| 'spotify_album'
|
||||||
|
| 'spotify_playlist'
|
||||||
|
| 'spotify_song'
|
||||||
|
| 'soundcloud_track'
|
||||||
|
| 'soundcloud_set'
|
||||||
|
| 'apple_music_playlist'
|
||||||
|
| 'apple_music_album'
|
||||||
|
| 'apple_music_song'
|
||||||
|
| 'vimeo_video'
|
||||||
|
| 'giphy_gif'
|
||||||
|
| 'tenor_gif'
|
||||||
|
|
||||||
|
export const externalEmbedLabels: Record<EmbedPlayerSource, string> = {
|
||||||
|
youtube: 'YouTube',
|
||||||
|
youtubeShorts: 'YouTube Shorts',
|
||||||
|
vimeo: 'Vimeo',
|
||||||
|
twitch: 'Twitch',
|
||||||
|
giphy: 'GIPHY',
|
||||||
|
tenor: 'Tenor',
|
||||||
|
spotify: 'Spotify',
|
||||||
|
appleMusic: 'Apple Music',
|
||||||
|
soundcloud: 'SoundCloud',
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface EmbedPlayerParams {
|
||||||
|
type: EmbedPlayerType
|
||||||
|
playerUri: string
|
||||||
|
isGif?: boolean
|
||||||
|
source: EmbedPlayerSource
|
||||||
|
metaUri?: string
|
||||||
|
hideDetails?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const giphyRegex = /media(?:[0-4]\.giphy\.com|\.giphy\.com)/i
|
||||||
|
const gifFilenameRegex = /^(\S+)\.(webp|gif|mp4)$/i
|
||||||
|
|
||||||
export function parseEmbedPlayerFromUrl(
|
export function parseEmbedPlayerFromUrl(
|
||||||
url: string,
|
url: string,
|
||||||
@@ -27,60 +71,88 @@ export function parseEmbedPlayerFromUrl(
|
|||||||
if (videoId) {
|
if (videoId) {
|
||||||
return {
|
return {
|
||||||
type: 'youtube_video',
|
type: 'youtube_video',
|
||||||
videoId,
|
source: 'youtube',
|
||||||
playerUri: `https://www.youtube.com/embed/${videoId}?autoplay=1`,
|
playerUri: `https://www.youtube.com/embed/${videoId}?autoplay=1&playsinline=1`,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (urlp.hostname === 'www.youtube.com' || urlp.hostname === 'youtube.com') {
|
if (
|
||||||
|
urlp.hostname === 'www.youtube.com' ||
|
||||||
|
urlp.hostname === 'youtube.com' ||
|
||||||
|
urlp.hostname === 'm.youtube.com'
|
||||||
|
) {
|
||||||
const [_, page, shortVideoId] = urlp.pathname.split('/')
|
const [_, page, shortVideoId] = urlp.pathname.split('/')
|
||||||
const videoId =
|
const videoId =
|
||||||
page === 'shorts' ? shortVideoId : (urlp.searchParams.get('v') as string)
|
page === 'shorts' ? shortVideoId : (urlp.searchParams.get('v') as string)
|
||||||
|
|
||||||
if (videoId) {
|
if (videoId) {
|
||||||
return {
|
return {
|
||||||
type: 'youtube_video',
|
type: page === 'shorts' ? 'youtube_short' : 'youtube_video',
|
||||||
videoId,
|
source: page === 'shorts' ? 'youtubeShorts' : 'youtube',
|
||||||
playerUri: `https://www.youtube.com/embed/${videoId}?autoplay=1`,
|
hideDetails: page === 'shorts' ? true : undefined,
|
||||||
|
playerUri: `https://www.youtube.com/embed/${videoId}?autoplay=1&playsinline=1`,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// twitch
|
// twitch
|
||||||
if (urlp.hostname === 'twitch.tv' || urlp.hostname === 'www.twitch.tv') {
|
if (
|
||||||
const parts = urlp.pathname.split('/')
|
urlp.hostname === 'twitch.tv' ||
|
||||||
if (parts.length === 2 && parts[1]) {
|
urlp.hostname === 'www.twitch.tv' ||
|
||||||
|
urlp.hostname === 'm.twitch.tv'
|
||||||
|
) {
|
||||||
|
const parent =
|
||||||
|
Platform.OS === 'web' ? window.location.hostname : 'localhost'
|
||||||
|
|
||||||
|
const [_, channelOrVideo, clipOrId, id] = urlp.pathname.split('/')
|
||||||
|
|
||||||
|
if (channelOrVideo === 'videos') {
|
||||||
return {
|
return {
|
||||||
type: 'twitch_live',
|
type: 'twitch_video',
|
||||||
channelId: parts[1],
|
source: 'twitch',
|
||||||
playerUri: `https://player.twitch.tv/?volume=0.5&!muted&autoplay&channel=${parts[1]}&parent=localhost`,
|
playerUri: `https://player.twitch.tv/?volume=0.5&!muted&autoplay&video=${clipOrId}&parent=${parent}`,
|
||||||
|
}
|
||||||
|
} else if (clipOrId === 'clip') {
|
||||||
|
return {
|
||||||
|
type: 'twitch_video',
|
||||||
|
source: 'twitch',
|
||||||
|
playerUri: `https://clips.twitch.tv/embed?volume=0.5&autoplay=true&clip=${id}&parent=${parent}`,
|
||||||
|
}
|
||||||
|
} else if (channelOrVideo) {
|
||||||
|
return {
|
||||||
|
type: 'twitch_video',
|
||||||
|
source: 'twitch',
|
||||||
|
playerUri: `https://player.twitch.tv/?volume=0.5&!muted&autoplay&channel=${channelOrVideo}&parent=${parent}`,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// spotify
|
// spotify
|
||||||
if (urlp.hostname === 'open.spotify.com') {
|
if (urlp.hostname === 'open.spotify.com') {
|
||||||
const [_, type, id] = urlp.pathname.split('/')
|
const [_, typeOrLocale, idOrType, id] = urlp.pathname.split('/')
|
||||||
if (type && id) {
|
|
||||||
if (type === 'playlist') {
|
if (idOrType) {
|
||||||
|
if (typeOrLocale === 'playlist' || idOrType === 'playlist') {
|
||||||
return {
|
return {
|
||||||
type: 'spotify_playlist',
|
type: 'spotify_playlist',
|
||||||
playlistId: id,
|
source: 'spotify',
|
||||||
playerUri: `https://open.spotify.com/embed/playlist/${id}`,
|
playerUri: `https://open.spotify.com/embed/playlist/${
|
||||||
|
id ?? idOrType
|
||||||
|
}`,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (type === 'album') {
|
if (typeOrLocale === 'album' || idOrType === 'album') {
|
||||||
return {
|
return {
|
||||||
type: 'spotify_album',
|
type: 'spotify_album',
|
||||||
albumId: id,
|
source: 'spotify',
|
||||||
playerUri: `https://open.spotify.com/embed/album/${id}`,
|
playerUri: `https://open.spotify.com/embed/album/${id ?? idOrType}`,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (type === 'track') {
|
if (typeOrLocale === 'track' || idOrType === 'track') {
|
||||||
return {
|
return {
|
||||||
type: 'spotify_song',
|
type: 'spotify_song',
|
||||||
songId: id,
|
source: 'spotify',
|
||||||
playerUri: `https://open.spotify.com/embed/track/${id}`,
|
playerUri: `https://open.spotify.com/embed/track/${id ?? idOrType}`,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -97,20 +169,173 @@ export function parseEmbedPlayerFromUrl(
|
|||||||
if (trackOrSets === 'sets' && set) {
|
if (trackOrSets === 'sets' && set) {
|
||||||
return {
|
return {
|
||||||
type: 'soundcloud_set',
|
type: 'soundcloud_set',
|
||||||
user,
|
source: 'soundcloud',
|
||||||
set: set,
|
|
||||||
playerUri: `https://w.soundcloud.com/player/?url=${url}&auto_play=true&visual=false&hide_related=true`,
|
playerUri: `https://w.soundcloud.com/player/?url=${url}&auto_play=true&visual=false&hide_related=true`,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
type: 'soundcloud_track',
|
type: 'soundcloud_track',
|
||||||
user,
|
source: 'soundcloud',
|
||||||
track: trackOrSets,
|
|
||||||
playerUri: `https://w.soundcloud.com/player/?url=${url}&auto_play=true&visual=false&hide_related=true`,
|
playerUri: `https://w.soundcloud.com/player/?url=${url}&auto_play=true&visual=false&hide_related=true`,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
urlp.hostname === 'music.apple.com' ||
|
||||||
|
urlp.hostname === 'music.apple.com'
|
||||||
|
) {
|
||||||
|
// This should always have: locale, type (playlist or album), name, and id. We won't use spread since we want
|
||||||
|
// to check if the length is correct
|
||||||
|
const pathParams = urlp.pathname.split('/')
|
||||||
|
const type = pathParams[2]
|
||||||
|
const songId = urlp.searchParams.get('i')
|
||||||
|
|
||||||
|
if (pathParams.length === 5 && (type === 'playlist' || type === 'album')) {
|
||||||
|
// We want to append the songId to the end of the url if it exists
|
||||||
|
const embedUri = `https://embed.music.apple.com${urlp.pathname}${
|
||||||
|
urlp.search ? '?i=' + songId : ''
|
||||||
|
}`
|
||||||
|
|
||||||
|
if (type === 'playlist') {
|
||||||
|
return {
|
||||||
|
type: 'apple_music_playlist',
|
||||||
|
source: 'appleMusic',
|
||||||
|
playerUri: embedUri,
|
||||||
|
}
|
||||||
|
} else if (type === 'album') {
|
||||||
|
if (songId) {
|
||||||
|
return {
|
||||||
|
type: 'apple_music_song',
|
||||||
|
source: 'appleMusic',
|
||||||
|
playerUri: embedUri,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
type: 'apple_music_album',
|
||||||
|
source: 'appleMusic',
|
||||||
|
playerUri: embedUri,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (urlp.hostname === 'vimeo.com' || urlp.hostname === 'www.vimeo.com') {
|
||||||
|
const [_, videoId] = urlp.pathname.split('/')
|
||||||
|
if (videoId) {
|
||||||
|
return {
|
||||||
|
type: 'vimeo_video',
|
||||||
|
source: 'vimeo',
|
||||||
|
playerUri: `https://player.vimeo.com/video/${videoId}?autoplay=1`,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (urlp.hostname === 'giphy.com' || urlp.hostname === 'www.giphy.com') {
|
||||||
|
const [_, gifs, nameAndId] = urlp.pathname.split('/')
|
||||||
|
|
||||||
|
/*
|
||||||
|
* nameAndId is a string that consists of the name (dash separated) and the id of the gif (the last part of the name)
|
||||||
|
* We want to get the id of the gif, then direct to media.giphy.com/media/{id}/giphy.webp so we can
|
||||||
|
* use it in an <Image> component
|
||||||
|
*/
|
||||||
|
|
||||||
|
if (gifs === 'gifs' && nameAndId) {
|
||||||
|
const gifId = nameAndId.split('-').pop()
|
||||||
|
|
||||||
|
if (gifId) {
|
||||||
|
return {
|
||||||
|
type: 'giphy_gif',
|
||||||
|
source: 'giphy',
|
||||||
|
isGif: true,
|
||||||
|
hideDetails: true,
|
||||||
|
metaUri: `https://giphy.com/gifs/${gifId}`,
|
||||||
|
playerUri: `https://i.giphy.com/media/${gifId}/giphy.webp`,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// There are five possible hostnames that also can be giphy urls: media.giphy.com and media0-4.giphy.com
|
||||||
|
// These can include (presumably) a tracking id in the path name, so we have to check for that as well
|
||||||
|
if (giphyRegex.test(urlp.hostname)) {
|
||||||
|
// We can link directly to the gif, if its a proper link
|
||||||
|
const [_, media, trackingOrId, idOrFilename, filename] =
|
||||||
|
urlp.pathname.split('/')
|
||||||
|
|
||||||
|
if (media === 'media') {
|
||||||
|
if (idOrFilename && gifFilenameRegex.test(idOrFilename)) {
|
||||||
|
return {
|
||||||
|
type: 'giphy_gif',
|
||||||
|
source: 'giphy',
|
||||||
|
isGif: true,
|
||||||
|
hideDetails: true,
|
||||||
|
metaUri: `https://giphy.com/gifs/${trackingOrId}`,
|
||||||
|
playerUri: `https://i.giphy.com/media/${trackingOrId}/giphy.webp`,
|
||||||
|
}
|
||||||
|
} else if (filename && gifFilenameRegex.test(filename)) {
|
||||||
|
return {
|
||||||
|
type: 'giphy_gif',
|
||||||
|
source: 'giphy',
|
||||||
|
isGif: true,
|
||||||
|
hideDetails: true,
|
||||||
|
metaUri: `https://giphy.com/gifs/${idOrFilename}`,
|
||||||
|
playerUri: `https://i.giphy.com/media/${idOrFilename}/giphy.webp`,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Finally, we should see if it is a link to i.giphy.com. These links don't necessarily end in .gif but can also
|
||||||
|
// be .webp
|
||||||
|
if (urlp.hostname === 'i.giphy.com' || urlp.hostname === 'www.i.giphy.com') {
|
||||||
|
const [_, mediaOrFilename, filename] = urlp.pathname.split('/')
|
||||||
|
|
||||||
|
if (mediaOrFilename === 'media' && filename) {
|
||||||
|
const gifId = filename.split('.')[0]
|
||||||
|
return {
|
||||||
|
type: 'giphy_gif',
|
||||||
|
source: 'giphy',
|
||||||
|
isGif: true,
|
||||||
|
hideDetails: true,
|
||||||
|
metaUri: `https://giphy.com/gifs/${gifId}`,
|
||||||
|
playerUri: `https://i.giphy.com/media/${gifId}/giphy.webp`,
|
||||||
|
}
|
||||||
|
} else if (mediaOrFilename) {
|
||||||
|
const gifId = mediaOrFilename.split('.')[0]
|
||||||
|
return {
|
||||||
|
type: 'giphy_gif',
|
||||||
|
source: 'giphy',
|
||||||
|
isGif: true,
|
||||||
|
hideDetails: true,
|
||||||
|
metaUri: `https://giphy.com/gifs/${gifId}`,
|
||||||
|
playerUri: `https://i.giphy.com/media/${
|
||||||
|
mediaOrFilename.split('.')[0]
|
||||||
|
}/giphy.webp`,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (urlp.hostname === 'tenor.com' || urlp.hostname === 'www.tenor.com') {
|
||||||
|
const [_, pathOrIntl, pathOrFilename, intlFilename] =
|
||||||
|
urlp.pathname.split('/')
|
||||||
|
const isIntl = pathOrFilename === 'view'
|
||||||
|
const filename = isIntl ? intlFilename : pathOrFilename
|
||||||
|
|
||||||
|
if ((pathOrIntl === 'view' || pathOrFilename === 'view') && filename) {
|
||||||
|
const includesExt = filename.split('.').pop() === 'gif'
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: 'tenor_gif',
|
||||||
|
source: 'tenor',
|
||||||
|
isGif: true,
|
||||||
|
hideDetails: true,
|
||||||
|
playerUri: `${url}${!includesExt ? '.gif' : ''}`,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getPlayerHeight({
|
export function getPlayerHeight({
|
||||||
@@ -126,22 +351,53 @@ export function getPlayerHeight({
|
|||||||
|
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case 'youtube_video':
|
case 'youtube_video':
|
||||||
case 'twitch_live':
|
case 'twitch_video':
|
||||||
|
case 'vimeo_video':
|
||||||
return (width / 16) * 9
|
return (width / 16) * 9
|
||||||
|
case 'youtube_short':
|
||||||
|
if (SCREEN_HEIGHT < 600) {
|
||||||
|
return ((width / 9) * 16) / 1.75
|
||||||
|
} else {
|
||||||
|
return ((width / 9) * 16) / 1.5
|
||||||
|
}
|
||||||
case 'spotify_album':
|
case 'spotify_album':
|
||||||
return 380
|
case 'apple_music_album':
|
||||||
|
case 'apple_music_playlist':
|
||||||
case 'spotify_playlist':
|
case 'spotify_playlist':
|
||||||
return 360
|
case 'soundcloud_set':
|
||||||
|
return 380
|
||||||
case 'spotify_song':
|
case 'spotify_song':
|
||||||
if (width <= 300) {
|
if (width <= 300) {
|
||||||
return 180
|
return 155
|
||||||
}
|
}
|
||||||
return 232
|
return 232
|
||||||
case 'soundcloud_track':
|
case 'soundcloud_track':
|
||||||
return 165
|
return 165
|
||||||
case 'soundcloud_set':
|
case 'apple_music_song':
|
||||||
return 360
|
return 150
|
||||||
default:
|
default:
|
||||||
return width
|
return width
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getGifDims(
|
||||||
|
originalHeight: number,
|
||||||
|
originalWidth: number,
|
||||||
|
viewWidth: number,
|
||||||
|
) {
|
||||||
|
const scaledHeight = (originalHeight / originalWidth) * viewWidth
|
||||||
|
|
||||||
|
return {
|
||||||
|
height: scaledHeight > 250 ? 250 : scaledHeight,
|
||||||
|
width: (250 / scaledHeight) * viewWidth,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getGiphyMetaUri(url: URL) {
|
||||||
|
if (giphyRegex.test(url.hostname) || url.hostname === 'i.giphy.com') {
|
||||||
|
const params = parseEmbedPlayerFromUrl(url.toString())
|
||||||
|
if (params && params.type === 'giphy_gif') {
|
||||||
|
return params.metaUri
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
import {AppBskyRichtextFacet, RichText} from '@atproto/api'
|
||||||
|
import {linkRequiresWarning} from './url-helpers'
|
||||||
|
|
||||||
|
export function richTextToString(rt: RichText): string {
|
||||||
|
const {text, facets} = rt
|
||||||
|
|
||||||
|
if (!facets?.length) {
|
||||||
|
return text
|
||||||
|
}
|
||||||
|
|
||||||
|
let result = ''
|
||||||
|
|
||||||
|
for (const segment of rt.segments()) {
|
||||||
|
const link = segment.link
|
||||||
|
|
||||||
|
if (link && AppBskyRichtextFacet.validateLink(link).success) {
|
||||||
|
const href = link.uri
|
||||||
|
const text = segment.text
|
||||||
|
|
||||||
|
const requiresWarning = linkRequiresWarning(href, text)
|
||||||
|
|
||||||
|
result += !requiresWarning ? href : `[${text}](${href})`
|
||||||
|
} else {
|
||||||
|
result += segment.text
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
@@ -167,6 +167,7 @@ export const s = StyleSheet.create({
|
|||||||
flexGrow1: {flexGrow: 1},
|
flexGrow1: {flexGrow: 1},
|
||||||
alignCenter: {alignItems: 'center'},
|
alignCenter: {alignItems: 'center'},
|
||||||
alignBaseline: {alignItems: 'baseline'},
|
alignBaseline: {alignItems: 'baseline'},
|
||||||
|
justifyCenter: {justifyContent: 'center'},
|
||||||
|
|
||||||
// position
|
// position
|
||||||
absolute: {position: 'absolute'},
|
absolute: {position: 'absolute'},
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ export const defaultTheme: Theme = {
|
|||||||
postCtrl: '#71768A',
|
postCtrl: '#71768A',
|
||||||
brandText: '#0066FF',
|
brandText: '#0066FF',
|
||||||
emptyStateIcon: '#B6B6C9',
|
emptyStateIcon: '#B6B6C9',
|
||||||
|
borderLinkHover: '#cac1c1',
|
||||||
},
|
},
|
||||||
primary: {
|
primary: {
|
||||||
background: colors.blue3,
|
background: colors.blue3,
|
||||||
@@ -310,6 +311,7 @@ export const darkTheme: Theme = {
|
|||||||
postCtrl: '#707489',
|
postCtrl: '#707489',
|
||||||
brandText: '#0085ff',
|
brandText: '#0085ff',
|
||||||
emptyStateIcon: colors.gray4,
|
emptyStateIcon: colors.gray4,
|
||||||
|
borderLinkHover: colors.gray5,
|
||||||
},
|
},
|
||||||
primary: {
|
primary: {
|
||||||
...defaultTheme.palette.primary,
|
...defaultTheme.palette.primary,
|
||||||
|
|||||||
@@ -5,7 +5,9 @@ import {AppLanguage} from '#/locale/languages'
|
|||||||
|
|
||||||
test('sanitizeAppLanguageSetting', () => {
|
test('sanitizeAppLanguageSetting', () => {
|
||||||
expect(sanitizeAppLanguageSetting('en')).toBe(AppLanguage.en)
|
expect(sanitizeAppLanguageSetting('en')).toBe(AppLanguage.en)
|
||||||
|
expect(sanitizeAppLanguageSetting('pt-BR')).toBe(AppLanguage.pt_BR)
|
||||||
expect(sanitizeAppLanguageSetting('hi')).toBe(AppLanguage.hi)
|
expect(sanitizeAppLanguageSetting('hi')).toBe(AppLanguage.hi)
|
||||||
|
expect(sanitizeAppLanguageSetting('id')).toBe(AppLanguage.id)
|
||||||
expect(sanitizeAppLanguageSetting('foo')).toBe(AppLanguage.en)
|
expect(sanitizeAppLanguageSetting('foo')).toBe(AppLanguage.en)
|
||||||
expect(sanitizeAppLanguageSetting('en,foo')).toBe(AppLanguage.en)
|
expect(sanitizeAppLanguageSetting('en,foo')).toBe(AppLanguage.en)
|
||||||
expect(sanitizeAppLanguageSetting('foo,en')).toBe(AppLanguage.en)
|
expect(sanitizeAppLanguageSetting('foo,en')).toBe(AppLanguage.en)
|
||||||
|
|||||||
@@ -110,20 +110,28 @@ export function sanitizeAppLanguageSetting(appLanguage: string): AppLanguage {
|
|||||||
switch (lang) {
|
switch (lang) {
|
||||||
case 'en':
|
case 'en':
|
||||||
return AppLanguage.en
|
return AppLanguage.en
|
||||||
case 'hi':
|
// DISABLED until this translation is fixed -prf
|
||||||
return AppLanguage.hi
|
// case 'de':
|
||||||
case 'ja':
|
// return AppLanguage.de
|
||||||
return AppLanguage.ja
|
|
||||||
case 'fr':
|
|
||||||
return AppLanguage.fr
|
|
||||||
case 'de':
|
|
||||||
return AppLanguage.de
|
|
||||||
case 'es':
|
case 'es':
|
||||||
return AppLanguage.es
|
return AppLanguage.es
|
||||||
|
case 'fr':
|
||||||
|
return AppLanguage.fr
|
||||||
|
case 'hi':
|
||||||
|
return AppLanguage.hi
|
||||||
|
case 'id':
|
||||||
|
return AppLanguage.id
|
||||||
|
case 'ja':
|
||||||
|
return AppLanguage.ja
|
||||||
|
case 'ko':
|
||||||
|
return AppLanguage.ko
|
||||||
|
case 'pt-BR':
|
||||||
|
return AppLanguage.pt_BR
|
||||||
|
case 'uk':
|
||||||
|
return AppLanguage.uk
|
||||||
default:
|
default:
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return AppLanguage.en
|
return AppLanguage.en
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,11 +3,16 @@ import {i18n} from '@lingui/core'
|
|||||||
|
|
||||||
import {useLanguagePrefs} from '#/state/preferences'
|
import {useLanguagePrefs} from '#/state/preferences'
|
||||||
import {messages as messagesEn} from '#/locale/locales/en/messages'
|
import {messages as messagesEn} from '#/locale/locales/en/messages'
|
||||||
|
// DISABLED until this translation is fixed -prf
|
||||||
|
// import {messages as messagesDe} from '#/locale/locales/de/messages'
|
||||||
|
import {messages as messagesId} from '#/locale/locales/id/messages'
|
||||||
|
import {messages as messagesEs} from '#/locale/locales/es/messages'
|
||||||
|
import {messages as messagesFr} from '#/locale/locales/fr/messages'
|
||||||
import {messages as messagesHi} from '#/locale/locales/hi/messages'
|
import {messages as messagesHi} from '#/locale/locales/hi/messages'
|
||||||
import {messages as messagesJa} from '#/locale/locales/ja/messages'
|
import {messages as messagesJa} from '#/locale/locales/ja/messages'
|
||||||
import {messages as messagesFr} from '#/locale/locales/fr/messages'
|
import {messages as messagesKo} from '#/locale/locales/ko/messages'
|
||||||
import {messages as messagesDe} from '#/locale/locales/de/messages'
|
import {messages as messagesPt_BR} from '#/locale/locales/pt-BR/messages'
|
||||||
import {messages as messagesEs} from '#/locale/locales/de/messages'
|
import {messages as messagesUk} from '#/locale/locales/uk/messages'
|
||||||
|
|
||||||
import {sanitizeAppLanguageSetting} from '#/locale/helpers'
|
import {sanitizeAppLanguageSetting} from '#/locale/helpers'
|
||||||
import {AppLanguage} from '#/locale/languages'
|
import {AppLanguage} from '#/locale/languages'
|
||||||
@@ -17,24 +22,41 @@ import {AppLanguage} from '#/locale/languages'
|
|||||||
*/
|
*/
|
||||||
export async function dynamicActivate(locale: AppLanguage) {
|
export async function dynamicActivate(locale: AppLanguage) {
|
||||||
switch (locale) {
|
switch (locale) {
|
||||||
case AppLanguage.hi: {
|
// DISABLED until this translation is fixed -prf
|
||||||
i18n.loadAndActivate({locale, messages: messagesHi})
|
// case AppLanguage.de: {
|
||||||
break
|
// i18n.loadAndActivate({locale, messages: messagesDe})
|
||||||
}
|
// break
|
||||||
case AppLanguage.ja: {
|
// }
|
||||||
i18n.loadAndActivate({locale, messages: messagesJa})
|
case AppLanguage.es: {
|
||||||
|
i18n.loadAndActivate({locale, messages: messagesEs})
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
case AppLanguage.fr: {
|
case AppLanguage.fr: {
|
||||||
i18n.loadAndActivate({locale, messages: messagesFr})
|
i18n.loadAndActivate({locale, messages: messagesFr})
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
case AppLanguage.de: {
|
case AppLanguage.hi: {
|
||||||
i18n.loadAndActivate({locale, messages: messagesDe})
|
i18n.loadAndActivate({locale, messages: messagesHi})
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
case AppLanguage.es: {
|
case AppLanguage.id: {
|
||||||
i18n.loadAndActivate({locale, messages: messagesEs})
|
i18n.loadAndActivate({locale, messages: messagesId})
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case AppLanguage.ja: {
|
||||||
|
i18n.loadAndActivate({locale, messages: messagesJa})
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case AppLanguage.ko: {
|
||||||
|
i18n.loadAndActivate({locale, messages: messagesKo})
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case AppLanguage.pt_BR: {
|
||||||
|
i18n.loadAndActivate({locale, messages: messagesPt_BR})
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case AppLanguage.uk: {
|
||||||
|
i18n.loadAndActivate({locale, messages: messagesUk})
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
default: {
|
default: {
|
||||||
|
|||||||
@@ -12,24 +12,41 @@ export async function dynamicActivate(locale: AppLanguage) {
|
|||||||
let mod: any
|
let mod: any
|
||||||
|
|
||||||
switch (locale) {
|
switch (locale) {
|
||||||
case AppLanguage.hi: {
|
// DISABLED until this translation is fixed -prf
|
||||||
mod = await import(`./locales/hi/messages`)
|
// case AppLanguage.de: {
|
||||||
break
|
// mod = await import(`./locales/de/messages`)
|
||||||
}
|
// break
|
||||||
case AppLanguage.ja: {
|
// }
|
||||||
mod = await import(`./locales/ja/messages`)
|
case AppLanguage.es: {
|
||||||
|
mod = await import(`./locales/es/messages`)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
case AppLanguage.fr: {
|
case AppLanguage.fr: {
|
||||||
mod = await import(`./locales/fr/messages`)
|
mod = await import(`./locales/fr/messages`)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
case AppLanguage.de: {
|
case AppLanguage.hi: {
|
||||||
mod = await import(`./locales/de/messages`)
|
mod = await import(`./locales/hi/messages`)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
case AppLanguage.es: {
|
case AppLanguage.id: {
|
||||||
mod = await import(`./locales/es/messages`)
|
mod = await import(`./locales/id/messages`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case AppLanguage.ja: {
|
||||||
|
mod = await import(`./locales/ja/messages`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case AppLanguage.ko: {
|
||||||
|
mod = await import(`./locales/ko/messages`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case AppLanguage.pt_BR: {
|
||||||
|
mod = await import(`./locales/pt-BR/messages`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case AppLanguage.uk: {
|
||||||
|
mod = await import(`./locales/uk/messages`)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
default: {
|
default: {
|
||||||
|
|||||||
@@ -6,11 +6,16 @@ interface Language {
|
|||||||
|
|
||||||
export enum AppLanguage {
|
export enum AppLanguage {
|
||||||
en = 'en',
|
en = 'en',
|
||||||
hi = 'hi',
|
// DISABLED until this translation is fixed -prf
|
||||||
ja = 'ja',
|
// de = 'de',
|
||||||
fr = 'fr',
|
|
||||||
de = 'de',
|
|
||||||
es = 'es',
|
es = 'es',
|
||||||
|
fr = 'fr',
|
||||||
|
hi = 'hi',
|
||||||
|
id = 'id',
|
||||||
|
ja = 'ja',
|
||||||
|
ko = 'ko',
|
||||||
|
pt_BR = 'pt-BR',
|
||||||
|
uk = 'uk',
|
||||||
}
|
}
|
||||||
|
|
||||||
interface AppLanguageConfig {
|
interface AppLanguageConfig {
|
||||||
@@ -20,11 +25,16 @@ interface AppLanguageConfig {
|
|||||||
|
|
||||||
export const APP_LANGUAGES: AppLanguageConfig[] = [
|
export const APP_LANGUAGES: AppLanguageConfig[] = [
|
||||||
{code2: AppLanguage.en, name: 'English'},
|
{code2: AppLanguage.en, name: 'English'},
|
||||||
{code2: AppLanguage.hi, name: 'हिंदी'},
|
// DISABLED until this translation is fixed -prf
|
||||||
{code2: AppLanguage.ja, name: '日本語'},
|
// {code2: AppLanguage.de, name: 'Deutsch'},
|
||||||
{code2: AppLanguage.fr, name: 'Français'},
|
|
||||||
{code2: AppLanguage.de, name: 'Deutsch'},
|
|
||||||
{code2: AppLanguage.es, name: 'Español'},
|
{code2: AppLanguage.es, name: 'Español'},
|
||||||
|
{code2: AppLanguage.fr, name: 'Français'},
|
||||||
|
{code2: AppLanguage.hi, name: 'हिंदी'},
|
||||||
|
{code2: AppLanguage.id, name: 'Bahasa Indonesia'},
|
||||||
|
{code2: AppLanguage.ja, name: '日本語'},
|
||||||
|
{code2: AppLanguage.ko, name: '한국어'},
|
||||||
|
{code2: AppLanguage.pt_BR, name: 'Português (BR)'},
|
||||||
|
{code2: AppLanguage.uk, name: 'Українська'},
|
||||||
]
|
]
|
||||||
|
|
||||||
export const LANGUAGES: Language[] = [
|
export const LANGUAGES: Language[] = [
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
// HACK
|
||||||
|
// expo-modules-core tries to require('crypto') in uuid.web.js
|
||||||
|
// and while it tries to detect web crypto before doing so, our
|
||||||
|
// build fails when it tries to do this require. We use a babel
|
||||||
|
// and tsconfig alias to direct it here
|
||||||
|
// -prf
|
||||||
|
export default crypto
|
||||||
@@ -14,5 +14,7 @@ export const isMobileWeb =
|
|||||||
global.window.matchMedia(isMobileWebMediaQuery)?.matches
|
global.window.matchMedia(isMobileWebMediaQuery)?.matches
|
||||||
|
|
||||||
export const deviceLocales = dedupArray(
|
export const deviceLocales = dedupArray(
|
||||||
getLocales?.().map?.(locale => locale.languageCode),
|
getLocales?.()
|
||||||
)
|
.map?.(locale => locale.languageCode)
|
||||||
|
.filter(code => typeof code === 'string'),
|
||||||
|
) as string[]
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ export const router = new Router({
|
|||||||
AppPasswords: '/settings/app-passwords',
|
AppPasswords: '/settings/app-passwords',
|
||||||
PreferencesHomeFeed: '/settings/home-feed',
|
PreferencesHomeFeed: '/settings/home-feed',
|
||||||
PreferencesThreads: '/settings/threads',
|
PreferencesThreads: '/settings/threads',
|
||||||
|
PreferencesExternalEmbeds: '/settings/external-embeds',
|
||||||
SavedFeeds: '/settings/saved-feeds',
|
SavedFeeds: '/settings/saved-feeds',
|
||||||
Support: '/support',
|
Support: '/support',
|
||||||
PrivacyPolicy: '/support/privacy',
|
PrivacyPolicy: '/support/privacy',
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {Image as RNImage} from 'react-native-image-crop-picker'
|
|||||||
import {ImageModel} from '#/state/models/media/image'
|
import {ImageModel} from '#/state/models/media/image'
|
||||||
import {GalleryModel} from '#/state/models/media/gallery'
|
import {GalleryModel} from '#/state/models/media/gallery'
|
||||||
import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback'
|
import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback'
|
||||||
|
import {EmbedPlayerSource} from '#/lib/strings/embed-player.ts'
|
||||||
import {ThreadgateSetting} from '../queries/threadgate'
|
import {ThreadgateSetting} from '../queries/threadgate'
|
||||||
|
|
||||||
export interface ConfirmModal {
|
export interface ConfirmModal {
|
||||||
@@ -180,6 +181,12 @@ export interface LinkWarningModal {
|
|||||||
href: string
|
href: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface EmbedConsentModal {
|
||||||
|
name: 'embed-consent'
|
||||||
|
source: EmbedPlayerSource
|
||||||
|
onAccept: () => void
|
||||||
|
}
|
||||||
|
|
||||||
export type Modal =
|
export type Modal =
|
||||||
// Account
|
// Account
|
||||||
| AddAppPasswordModal
|
| AddAppPasswordModal
|
||||||
@@ -223,6 +230,7 @@ export type Modal =
|
|||||||
// Generic
|
// Generic
|
||||||
| ConfirmModal
|
| ConfirmModal
|
||||||
| LinkWarningModal
|
| LinkWarningModal
|
||||||
|
| EmbedConsentModal
|
||||||
|
|
||||||
const ModalContext = React.createContext<{
|
const ModalContext = React.createContext<{
|
||||||
isModalActive: boolean
|
isModalActive: boolean
|
||||||
|
|||||||
@@ -108,6 +108,8 @@ export function transform(legacy: Partial<LegacySchema>): Schema {
|
|||||||
onboarding: {
|
onboarding: {
|
||||||
step: legacy.onboarding?.step || defaults.onboarding.step,
|
step: legacy.onboarding?.step || defaults.onboarding.step,
|
||||||
},
|
},
|
||||||
|
hiddenPosts: defaults.hiddenPosts,
|
||||||
|
externalEmbeds: defaults.externalEmbeds,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import {z} from 'zod'
|
import {z} from 'zod'
|
||||||
import {deviceLocales} from '#/platform/detection'
|
import {deviceLocales} from '#/platform/detection'
|
||||||
|
|
||||||
|
const externalEmbedOptions = ['show', 'hide'] as const
|
||||||
|
|
||||||
// only data needed for rendering account page
|
// only data needed for rendering account page
|
||||||
const accountSchema = z.object({
|
const accountSchema = z.object({
|
||||||
service: z.string(),
|
service: z.string(),
|
||||||
@@ -30,6 +32,19 @@ export const schema = z.object({
|
|||||||
appLanguage: z.string(),
|
appLanguage: z.string(),
|
||||||
}),
|
}),
|
||||||
requireAltTextEnabled: z.boolean(), // should move to server
|
requireAltTextEnabled: z.boolean(), // should move to server
|
||||||
|
externalEmbeds: z
|
||||||
|
.object({
|
||||||
|
giphy: z.enum(externalEmbedOptions).optional(),
|
||||||
|
tenor: z.enum(externalEmbedOptions).optional(),
|
||||||
|
youtube: z.enum(externalEmbedOptions).optional(),
|
||||||
|
youtubeShorts: z.enum(externalEmbedOptions).optional(),
|
||||||
|
twitch: z.enum(externalEmbedOptions).optional(),
|
||||||
|
vimeo: z.enum(externalEmbedOptions).optional(),
|
||||||
|
spotify: z.enum(externalEmbedOptions).optional(),
|
||||||
|
appleMusic: z.enum(externalEmbedOptions).optional(),
|
||||||
|
soundcloud: z.enum(externalEmbedOptions).optional(),
|
||||||
|
})
|
||||||
|
.optional(),
|
||||||
mutedThreads: z.array(z.string()), // should move to server
|
mutedThreads: z.array(z.string()), // should move to server
|
||||||
invites: z.object({
|
invites: z.object({
|
||||||
copiedInvites: z.array(z.string()),
|
copiedInvites: z.array(z.string()),
|
||||||
@@ -37,6 +52,7 @@ export const schema = z.object({
|
|||||||
onboarding: z.object({
|
onboarding: z.object({
|
||||||
step: z.string(),
|
step: z.string(),
|
||||||
}),
|
}),
|
||||||
|
hiddenPosts: z.array(z.string()).optional(), // should move to server
|
||||||
})
|
})
|
||||||
export type Schema = z.infer<typeof schema>
|
export type Schema = z.infer<typeof schema>
|
||||||
|
|
||||||
@@ -59,6 +75,7 @@ export const defaults: Schema = {
|
|||||||
appLanguage: deviceLocales[0] || 'en',
|
appLanguage: deviceLocales[0] || 'en',
|
||||||
},
|
},
|
||||||
requireAltTextEnabled: false,
|
requireAltTextEnabled: false,
|
||||||
|
externalEmbeds: {},
|
||||||
mutedThreads: [],
|
mutedThreads: [],
|
||||||
invites: {
|
invites: {
|
||||||
copiedInvites: [],
|
copiedInvites: [],
|
||||||
@@ -66,4 +83,5 @@ export const defaults: Schema = {
|
|||||||
onboarding: {
|
onboarding: {
|
||||||
step: 'Home',
|
step: 'Home',
|
||||||
},
|
},
|
||||||
|
hiddenPosts: [],
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,54 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import * as persisted from '#/state/persisted'
|
||||||
|
import {EmbedPlayerSource} from 'lib/strings/embed-player'
|
||||||
|
|
||||||
|
type StateContext = persisted.Schema['externalEmbeds']
|
||||||
|
type SetContext = (source: EmbedPlayerSource, value: 'show' | 'hide') => void
|
||||||
|
|
||||||
|
const stateContext = React.createContext<StateContext>(
|
||||||
|
persisted.defaults.externalEmbeds,
|
||||||
|
)
|
||||||
|
const setContext = React.createContext<SetContext>({} as SetContext)
|
||||||
|
|
||||||
|
export function Provider({children}: React.PropsWithChildren<{}>) {
|
||||||
|
const [state, setState] = React.useState(persisted.get('externalEmbeds'))
|
||||||
|
|
||||||
|
const setStateWrapped = React.useCallback(
|
||||||
|
(source: EmbedPlayerSource, value: 'show' | 'hide') => {
|
||||||
|
setState(prev => {
|
||||||
|
persisted.write('externalEmbeds', {
|
||||||
|
...prev,
|
||||||
|
[source]: value,
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
...prev,
|
||||||
|
[source]: value,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
[setState],
|
||||||
|
)
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
return persisted.onUpdate(() => {
|
||||||
|
setState(persisted.get('externalEmbeds'))
|
||||||
|
})
|
||||||
|
}, [setStateWrapped])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<stateContext.Provider value={state}>
|
||||||
|
<setContext.Provider value={setStateWrapped}>
|
||||||
|
{children}
|
||||||
|
</setContext.Provider>
|
||||||
|
</stateContext.Provider>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useExternalEmbedsPrefs() {
|
||||||
|
return React.useContext(stateContext)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useSetExternalEmbedPref() {
|
||||||
|
return React.useContext(setContext)
|
||||||
|
}
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import * as persisted from '#/state/persisted'
|
||||||
|
|
||||||
|
type SetStateCb = (
|
||||||
|
s: persisted.Schema['hiddenPosts'],
|
||||||
|
) => persisted.Schema['hiddenPosts']
|
||||||
|
type StateContext = persisted.Schema['hiddenPosts']
|
||||||
|
type ApiContext = {
|
||||||
|
hidePost: ({uri}: {uri: string}) => void
|
||||||
|
unhidePost: ({uri}: {uri: string}) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const stateContext = React.createContext<StateContext>(
|
||||||
|
persisted.defaults.hiddenPosts,
|
||||||
|
)
|
||||||
|
const apiContext = React.createContext<ApiContext>({
|
||||||
|
hidePost: () => {},
|
||||||
|
unhidePost: () => {},
|
||||||
|
})
|
||||||
|
|
||||||
|
export function Provider({children}: React.PropsWithChildren<{}>) {
|
||||||
|
const [state, setState] = React.useState(persisted.get('hiddenPosts'))
|
||||||
|
|
||||||
|
const setStateWrapped = React.useCallback(
|
||||||
|
(fn: SetStateCb) => {
|
||||||
|
const s = fn(persisted.get('hiddenPosts'))
|
||||||
|
setState(s)
|
||||||
|
persisted.write('hiddenPosts', s)
|
||||||
|
},
|
||||||
|
[setState],
|
||||||
|
)
|
||||||
|
|
||||||
|
const api = React.useMemo(
|
||||||
|
() => ({
|
||||||
|
hidePost: ({uri}: {uri: string}) => {
|
||||||
|
setStateWrapped(s => [...(s || []), uri])
|
||||||
|
},
|
||||||
|
unhidePost: ({uri}: {uri: string}) => {
|
||||||
|
setStateWrapped(s => (s || []).filter(u => u !== uri))
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
[setStateWrapped],
|
||||||
|
)
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
return persisted.onUpdate(() => {
|
||||||
|
setState(persisted.get('hiddenPosts'))
|
||||||
|
})
|
||||||
|
}, [setStateWrapped])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<stateContext.Provider value={state}>
|
||||||
|
<apiContext.Provider value={api}>{children}</apiContext.Provider>
|
||||||
|
</stateContext.Provider>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useHiddenPosts() {
|
||||||
|
return React.useContext(stateContext)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useHiddenPostsApi() {
|
||||||
|
return React.useContext(apiContext)
|
||||||
|
}
|
||||||
@@ -1,17 +1,28 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import {Provider as LanguagesProvider} from './languages'
|
import {Provider as LanguagesProvider} from './languages'
|
||||||
import {Provider as AltTextRequiredProvider} from '../preferences/alt-text-required'
|
import {Provider as AltTextRequiredProvider} from '../preferences/alt-text-required'
|
||||||
|
import {Provider as HiddenPostsProvider} from '../preferences/hidden-posts'
|
||||||
|
import {Provider as ExternalEmbedsProvider} from './external-embeds-prefs'
|
||||||
|
|
||||||
export {useLanguagePrefs, useLanguagePrefsApi} from './languages'
|
export {useLanguagePrefs, useLanguagePrefsApi} from './languages'
|
||||||
export {
|
export {
|
||||||
useRequireAltTextEnabled,
|
useRequireAltTextEnabled,
|
||||||
useSetRequireAltTextEnabled,
|
useSetRequireAltTextEnabled,
|
||||||
} from './alt-text-required'
|
} from './alt-text-required'
|
||||||
|
export {
|
||||||
|
useExternalEmbedsPrefs,
|
||||||
|
useSetExternalEmbedPref,
|
||||||
|
} from './external-embeds-prefs'
|
||||||
|
export * from './hidden-posts'
|
||||||
|
|
||||||
export function Provider({children}: React.PropsWithChildren<{}>) {
|
export function Provider({children}: React.PropsWithChildren<{}>) {
|
||||||
return (
|
return (
|
||||||
<LanguagesProvider>
|
<LanguagesProvider>
|
||||||
<AltTextRequiredProvider>{children}</AltTextRequiredProvider>
|
<AltTextRequiredProvider>
|
||||||
|
<ExternalEmbedsProvider>
|
||||||
|
<HiddenPostsProvider>{children}</HiddenPostsProvider>
|
||||||
|
</ExternalEmbedsProvider>
|
||||||
|
</AltTextRequiredProvider>
|
||||||
</LanguagesProvider>
|
</LanguagesProvider>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,6 +24,8 @@ export function useActorAutocompleteQuery(prefix: string) {
|
|||||||
const {data: follows, isFetching} = useMyFollowsQuery()
|
const {data: follows, isFetching} = useMyFollowsQuery()
|
||||||
const moderationOpts = useModerationOpts()
|
const moderationOpts = useModerationOpts()
|
||||||
|
|
||||||
|
prefix = prefix.toLowerCase()
|
||||||
|
|
||||||
return useQuery<AppBskyActorDefs.ProfileViewBasic[]>({
|
return useQuery<AppBskyActorDefs.ProfileViewBasic[]>({
|
||||||
staleTime: STALE.MINUTES.ONE,
|
staleTime: STALE.MINUTES.ONE,
|
||||||
queryKey: RQKEY(prefix || ''),
|
queryKey: RQKEY(prefix || ''),
|
||||||
@@ -112,7 +114,7 @@ function computeSuggestions(
|
|||||||
}
|
}
|
||||||
return items.filter(profile => {
|
return items.filter(profile => {
|
||||||
const mod = moderateProfile(profile, moderationOpts)
|
const mod = moderateProfile(profile, moderationOpts)
|
||||||
return !mod.account.filter
|
return !mod.account.filter && mod.account.cause?.type !== 'muted'
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ export const RQKEY = () => ['app-passwords']
|
|||||||
export function useAppPasswordsQuery() {
|
export function useAppPasswordsQuery() {
|
||||||
return useQuery({
|
return useQuery({
|
||||||
staleTime: STALE.MINUTES.FIVE,
|
staleTime: STALE.MINUTES.FIVE,
|
||||||
refetchInterval: STALE.MINUTES.ONE,
|
|
||||||
queryKey: RQKEY(),
|
queryKey: RQKEY(),
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
const res = await getAgent().com.atproto.server.listAppPasswords({})
|
const res = await getAgent().com.atproto.server.listAppPasswords({})
|
||||||
|
|||||||
@@ -218,11 +218,13 @@ const FOLLOWING_FEED_STUB: FeedSourceInfo = {
|
|||||||
export function usePinnedFeedsInfos(): {
|
export function usePinnedFeedsInfos(): {
|
||||||
feeds: FeedSourceInfo[]
|
feeds: FeedSourceInfo[]
|
||||||
hasPinnedCustom: boolean
|
hasPinnedCustom: boolean
|
||||||
|
isLoading: boolean
|
||||||
} {
|
} {
|
||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
const [tabs, setTabs] = React.useState<FeedSourceInfo[]>([
|
const [tabs, setTabs] = React.useState<FeedSourceInfo[]>([
|
||||||
FOLLOWING_FEED_STUB,
|
FOLLOWING_FEED_STUB,
|
||||||
])
|
])
|
||||||
|
const [isLoading, setLoading] = React.useState(true)
|
||||||
const {data: preferences} = usePreferencesQuery()
|
const {data: preferences} = usePreferencesQuery()
|
||||||
|
|
||||||
const hasPinnedCustom = React.useMemo<boolean>(() => {
|
const hasPinnedCustom = React.useMemo<boolean>(() => {
|
||||||
@@ -284,10 +286,11 @@ export function usePinnedFeedsInfos(): {
|
|||||||
) as FeedSourceInfo[]
|
) as FeedSourceInfo[]
|
||||||
|
|
||||||
setTabs([FOLLOWING_FEED_STUB].concat(views))
|
setTabs([FOLLOWING_FEED_STUB].concat(views))
|
||||||
|
setLoading(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
fetchFeedInfo()
|
fetchFeedInfo()
|
||||||
}, [queryClient, setTabs, preferences?.feeds?.pinned])
|
}, [queryClient, setTabs, preferences?.feeds?.pinned])
|
||||||
|
|
||||||
return {feeds: tabs, hasPinnedCustom}
|
return {feeds: tabs, hasPinnedCustom, isLoading}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ export type InviteCodesQueryResponse = Exclude<
|
|||||||
export function useInviteCodesQuery() {
|
export function useInviteCodesQuery() {
|
||||||
return useQuery({
|
return useQuery({
|
||||||
staleTime: STALE.MINUTES.FIVE,
|
staleTime: STALE.MINUTES.FIVE,
|
||||||
refetchInterval: STALE.MINUTES.FIVE,
|
|
||||||
queryKey: ['inviteCodes'],
|
queryKey: ['inviteCodes'],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
const res = await getAgent()
|
const res = await getAgent()
|
||||||
|
|||||||
@@ -35,4 +35,5 @@ export interface CachedFeedPage {
|
|||||||
usableInFeed: boolean
|
usableInFeed: boolean
|
||||||
syncedAt: Date
|
syncedAt: Date
|
||||||
data: FeedPage | undefined
|
data: FeedPage | undefined
|
||||||
|
unreadCount: number
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import {useMutedThreads} from '#/state/muted-threads'
|
|||||||
import {RQKEY as RQKEY_NOTIFS} from './feed'
|
import {RQKEY as RQKEY_NOTIFS} from './feed'
|
||||||
import {logger} from '#/logger'
|
import {logger} from '#/logger'
|
||||||
import {truncateAndInvalidate} from '../util'
|
import {truncateAndInvalidate} from '../util'
|
||||||
|
import {AppState} from 'react-native'
|
||||||
|
|
||||||
const UPDATE_INTERVAL = 30 * 1e3 // 30sec
|
const UPDATE_INTERVAL = 30 * 1e3 // 30sec
|
||||||
|
|
||||||
@@ -24,7 +25,10 @@ type StateContext = string
|
|||||||
|
|
||||||
interface ApiContext {
|
interface ApiContext {
|
||||||
markAllRead: () => Promise<void>
|
markAllRead: () => Promise<void>
|
||||||
checkUnread: (opts?: {invalidate?: boolean}) => Promise<void>
|
checkUnread: (opts?: {
|
||||||
|
invalidate?: boolean
|
||||||
|
isPoll?: boolean
|
||||||
|
}) => Promise<void>
|
||||||
getCachedUnreadPage: () => FeedPage | undefined
|
getCachedUnreadPage: () => FeedPage | undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -49,6 +53,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
|
|||||||
usableInFeed: false,
|
usableInFeed: false,
|
||||||
syncedAt: new Date(),
|
syncedAt: new Date(),
|
||||||
data: undefined,
|
data: undefined,
|
||||||
|
unreadCount: 0,
|
||||||
})
|
})
|
||||||
|
|
||||||
// periodic sync
|
// periodic sync
|
||||||
@@ -57,7 +62,10 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
checkUnreadRef.current() // fire on init
|
checkUnreadRef.current() // fire on init
|
||||||
const interval = setInterval(checkUnreadRef.current, UPDATE_INTERVAL)
|
const interval = setInterval(
|
||||||
|
() => checkUnreadRef.current?.({isPoll: true}),
|
||||||
|
UPDATE_INTERVAL,
|
||||||
|
)
|
||||||
return () => clearInterval(interval)
|
return () => clearInterval(interval)
|
||||||
}, [hasSession])
|
}, [hasSession])
|
||||||
|
|
||||||
@@ -68,6 +76,12 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
|
|||||||
usableInFeed: false,
|
usableInFeed: false,
|
||||||
syncedAt: new Date(),
|
syncedAt: new Date(),
|
||||||
data: undefined,
|
data: undefined,
|
||||||
|
unreadCount:
|
||||||
|
data.event === '30+'
|
||||||
|
? 30
|
||||||
|
: data.event === ''
|
||||||
|
? 0
|
||||||
|
: parseInt(data.event, 10) || 1,
|
||||||
}
|
}
|
||||||
setNumUnread(data.event)
|
setNumUnread(data.event)
|
||||||
}
|
}
|
||||||
@@ -89,11 +103,28 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
|
|||||||
// update & broadcast
|
// update & broadcast
|
||||||
setNumUnread('')
|
setNumUnread('')
|
||||||
broadcast.postMessage({event: ''})
|
broadcast.postMessage({event: ''})
|
||||||
|
if (isNative) {
|
||||||
|
Notifications.setBadgeCountAsync(0)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
async checkUnread({invalidate}: {invalidate?: boolean} = {}) {
|
async checkUnread({
|
||||||
|
invalidate,
|
||||||
|
isPoll,
|
||||||
|
}: {invalidate?: boolean; isPoll?: boolean} = {}) {
|
||||||
try {
|
try {
|
||||||
if (!getAgent().session) return
|
if (!getAgent().session) return
|
||||||
|
if (AppState.currentState !== 'active') {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// reduce polling if unread count is set
|
||||||
|
if (isPoll && cacheRef.current?.unreadCount !== 0) {
|
||||||
|
// if hit 30+ then don't poll, otherwise reduce polling by 50%
|
||||||
|
if (cacheRef.current?.unreadCount >= 30 || Math.random() >= 0.5) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// count
|
// count
|
||||||
const page = await fetchPage({
|
const page = await fetchPage({
|
||||||
@@ -126,6 +157,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
|
|||||||
usableInFeed: !!invalidate, // will be used immediately
|
usableInFeed: !!invalidate, // will be used immediately
|
||||||
data: page,
|
data: page,
|
||||||
syncedAt: !lastIndexed || now > lastIndexed ? now : lastIndexed,
|
syncedAt: !lastIndexed || now > lastIndexed ? now : lastIndexed,
|
||||||
|
unreadCount,
|
||||||
}
|
}
|
||||||
|
|
||||||
// update & broadcast
|
// update & broadcast
|
||||||
|
|||||||
@@ -2,12 +2,12 @@ import {
|
|||||||
AppBskyNotificationListNotifications,
|
AppBskyNotificationListNotifications,
|
||||||
ModerationOpts,
|
ModerationOpts,
|
||||||
moderateProfile,
|
moderateProfile,
|
||||||
moderatePost,
|
|
||||||
AppBskyFeedDefs,
|
AppBskyFeedDefs,
|
||||||
AppBskyFeedPost,
|
AppBskyFeedPost,
|
||||||
AppBskyFeedRepost,
|
AppBskyFeedRepost,
|
||||||
AppBskyFeedLike,
|
AppBskyFeedLike,
|
||||||
} from '@atproto/api'
|
} from '@atproto/api'
|
||||||
|
import {moderatePost_wrapped as moderatePost} from '#/lib/moderatePost_wrapped'
|
||||||
import chunk from 'lodash.chunk'
|
import chunk from 'lodash.chunk'
|
||||||
import {QueryClient} from '@tanstack/react-query'
|
import {QueryClient} from '@tanstack/react-query'
|
||||||
import {getAgent} from '../../session'
|
import {getAgent} from '../../session'
|
||||||
@@ -156,7 +156,7 @@ async function fetchSubjects(
|
|||||||
): Promise<Map<string, AppBskyFeedDefs.PostView>> {
|
): Promise<Map<string, AppBskyFeedDefs.PostView>> {
|
||||||
const uris = new Set<string>()
|
const uris = new Set<string>()
|
||||||
for (const notif of groupedNotifs) {
|
for (const notif of groupedNotifs) {
|
||||||
if (notif.subjectUri) {
|
if (notif.subjectUri && !notif.subjectUri.includes('feed.generator')) {
|
||||||
uris.add(notif.subjectUri)
|
uris.add(notif.subjectUri)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -216,6 +216,8 @@ function getSubjectUri(
|
|||||||
? notif.record.subject?.uri
|
? notif.record.subject?.uri
|
||||||
: undefined
|
: undefined
|
||||||
}
|
}
|
||||||
|
} else if (type === 'feedgen-like') {
|
||||||
|
return notif.reasonSubject
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,6 @@
|
|||||||
import React, {useCallback, useEffect, useRef} from 'react'
|
import React, {useCallback, useEffect, useRef} from 'react'
|
||||||
import {
|
import {AppState} from 'react-native'
|
||||||
AppBskyFeedDefs,
|
import {AppBskyFeedDefs, AppBskyFeedPost, PostModeration} from '@atproto/api'
|
||||||
AppBskyFeedPost,
|
|
||||||
moderatePost,
|
|
||||||
PostModeration,
|
|
||||||
} from '@atproto/api'
|
|
||||||
import {
|
import {
|
||||||
useInfiniteQuery,
|
useInfiniteQuery,
|
||||||
InfiniteData,
|
InfiniteData,
|
||||||
@@ -12,6 +8,7 @@ import {
|
|||||||
QueryClient,
|
QueryClient,
|
||||||
useQueryClient,
|
useQueryClient,
|
||||||
} from '@tanstack/react-query'
|
} from '@tanstack/react-query'
|
||||||
|
import {moderatePost_wrapped as moderatePost} from '#/lib/moderatePost_wrapped'
|
||||||
import {useFeedTuners} from '../preferences/feed-tuners'
|
import {useFeedTuners} from '../preferences/feed-tuners'
|
||||||
import {FeedTuner, FeedTunerFn, NoopFeedTuner} from 'lib/api/feed-manip'
|
import {FeedTuner, FeedTunerFn, NoopFeedTuner} from 'lib/api/feed-manip'
|
||||||
import {FeedAPI, ReasonFeedSource} from 'lib/api/feed/types'
|
import {FeedAPI, ReasonFeedSource} from 'lib/api/feed/types'
|
||||||
@@ -316,6 +313,9 @@ export async function pollLatest(page: FeedPage | undefined) {
|
|||||||
if (!page) {
|
if (!page) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
if (AppState.currentState !== 'active') {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
logger.debug('usePostFeedQuery: pollLatest')
|
logger.debug('usePostFeedQuery: pollLatest')
|
||||||
const post = await page.api.peekLatest()
|
const post = await page.api.peekLatest()
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import {
|
|||||||
} from '#/state/queries/preferences/const'
|
} from '#/state/queries/preferences/const'
|
||||||
import {getModerationOpts} from '#/state/queries/preferences/moderation'
|
import {getModerationOpts} from '#/state/queries/preferences/moderation'
|
||||||
import {STALE} from '#/state/queries'
|
import {STALE} from '#/state/queries'
|
||||||
|
import {useHiddenPosts} from '#/state/preferences/hidden-posts'
|
||||||
|
|
||||||
export * from '#/state/queries/preferences/types'
|
export * from '#/state/queries/preferences/types'
|
||||||
export * from '#/state/queries/preferences/moderation'
|
export * from '#/state/queries/preferences/moderation'
|
||||||
@@ -30,7 +31,7 @@ export function usePreferencesQuery() {
|
|||||||
return useQuery({
|
return useQuery({
|
||||||
staleTime: STALE.SECONDS.FIFTEEN,
|
staleTime: STALE.SECONDS.FIFTEEN,
|
||||||
structuralSharing: true,
|
structuralSharing: true,
|
||||||
refetchInterval: STALE.SECONDS.FIFTEEN,
|
refetchOnWindowFocus: true,
|
||||||
queryKey: preferencesQueryKey,
|
queryKey: preferencesQueryKey,
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
const agent = getAgent()
|
const agent = getAgent()
|
||||||
@@ -94,15 +95,21 @@ export function usePreferencesQuery() {
|
|||||||
export function useModerationOpts() {
|
export function useModerationOpts() {
|
||||||
const {currentAccount} = useSession()
|
const {currentAccount} = useSession()
|
||||||
const prefs = usePreferencesQuery()
|
const prefs = usePreferencesQuery()
|
||||||
|
const hiddenPosts = useHiddenPosts()
|
||||||
const opts = useMemo(() => {
|
const opts = useMemo(() => {
|
||||||
if (!prefs.data) {
|
if (!prefs.data) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
return getModerationOpts({
|
const moderationOpts = getModerationOpts({
|
||||||
userDid: currentAccount?.did || '',
|
userDid: currentAccount?.did || '',
|
||||||
preferences: prefs.data,
|
preferences: prefs.data,
|
||||||
})
|
})
|
||||||
}, [currentAccount?.did, prefs.data])
|
|
||||||
|
return {
|
||||||
|
...moderationOpts,
|
||||||
|
hiddenPosts,
|
||||||
|
}
|
||||||
|
}, [currentAccount?.did, prefs.data, hiddenPosts])
|
||||||
return opts
|
return opts
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ export function useProfileQuery({did}: {did: string | undefined}) {
|
|||||||
// if you remove it, the UI infinite-loops
|
// if you remove it, the UI infinite-loops
|
||||||
// -prf
|
// -prf
|
||||||
staleTime: isCurrentAccount ? STALE.SECONDS.THIRTY : STALE.MINUTES.FIVE,
|
staleTime: isCurrentAccount ? STALE.SECONDS.THIRTY : STALE.MINUTES.FIVE,
|
||||||
refetchInterval: STALE.MINUTES.FIVE,
|
refetchOnWindowFocus: true,
|
||||||
queryKey: RQKEY(did || ''),
|
queryKey: RQKEY(did || ''),
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
const res = await getAgent().getProfile({actor: did || ''})
|
const res = await getAgent().getProfile({actor: did || ''})
|
||||||
|
|||||||
@@ -102,10 +102,21 @@ function createPersistSessionHandler(
|
|||||||
expired: boolean
|
expired: boolean
|
||||||
refreshedAccount: SessionAccount
|
refreshedAccount: SessionAccount
|
||||||
}) => void,
|
}) => void,
|
||||||
|
{
|
||||||
|
networkErrorCallback,
|
||||||
|
}: {
|
||||||
|
networkErrorCallback?: () => void
|
||||||
|
} = {},
|
||||||
): AtpPersistSessionHandler {
|
): AtpPersistSessionHandler {
|
||||||
return function persistSession(event, session) {
|
return function persistSession(event, session) {
|
||||||
const expired = event === 'expired' || event === 'create-failed'
|
const expired = event === 'expired' || event === 'create-failed'
|
||||||
|
|
||||||
|
if (event === 'network-error') {
|
||||||
|
logger.warn(`session: persistSessionHandler received network-error event`)
|
||||||
|
networkErrorCallback?.()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
const refreshedAccount: SessionAccount = {
|
const refreshedAccount: SessionAccount = {
|
||||||
service: account.service,
|
service: account.service,
|
||||||
did: session?.did || account.did,
|
did: session?.did || account.did,
|
||||||
@@ -125,9 +136,11 @@ function createPersistSessionHandler(
|
|||||||
event,
|
event,
|
||||||
did: refreshedAccount.did,
|
did: refreshedAccount.did,
|
||||||
handle: refreshedAccount.handle,
|
handle: refreshedAccount.handle,
|
||||||
|
service: refreshedAccount.service,
|
||||||
})
|
})
|
||||||
|
|
||||||
if (expired) {
|
if (expired) {
|
||||||
|
logger.warn(`session: expired`)
|
||||||
emitSessionDropped()
|
emitSessionDropped()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -179,16 +192,26 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
|
|||||||
[setStateAndPersist],
|
[setStateAndPersist],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const clearCurrentAccount = React.useCallback(() => {
|
||||||
|
logger.debug(
|
||||||
|
`session: clear current account`,
|
||||||
|
{},
|
||||||
|
logger.DebugContext.session,
|
||||||
|
)
|
||||||
|
__globalAgent = PUBLIC_BSKY_AGENT
|
||||||
|
queryClient.clear()
|
||||||
|
setStateAndPersist(s => ({
|
||||||
|
...s,
|
||||||
|
currentAccount: undefined,
|
||||||
|
}))
|
||||||
|
}, [setStateAndPersist, queryClient])
|
||||||
|
|
||||||
const createAccount = React.useCallback<ApiContext['createAccount']>(
|
const createAccount = React.useCallback<ApiContext['createAccount']>(
|
||||||
async ({service, email, password, handle, inviteCode}: any) => {
|
async ({service, email, password, handle, inviteCode}: any) => {
|
||||||
logger.debug(
|
logger.info(`session: creating account`, {
|
||||||
`session: creating account`,
|
service,
|
||||||
{
|
handle,
|
||||||
service,
|
})
|
||||||
handle,
|
|
||||||
},
|
|
||||||
logger.DebugContext.session,
|
|
||||||
)
|
|
||||||
track('Try Create Account')
|
track('Try Create Account')
|
||||||
|
|
||||||
const agent = new BskyAgent({service})
|
const agent = new BskyAgent({service})
|
||||||
@@ -215,9 +238,13 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
agent.setPersistSessionHandler(
|
agent.setPersistSessionHandler(
|
||||||
createPersistSessionHandler(account, ({expired, refreshedAccount}) => {
|
createPersistSessionHandler(
|
||||||
upsertAccount(refreshedAccount, expired)
|
account,
|
||||||
}),
|
({expired, refreshedAccount}) => {
|
||||||
|
upsertAccount(refreshedAccount, expired)
|
||||||
|
},
|
||||||
|
{networkErrorCallback: clearCurrentAccount},
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
__globalAgent = agent
|
__globalAgent = agent
|
||||||
@@ -234,7 +261,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
|
|||||||
)
|
)
|
||||||
track('Create Account')
|
track('Create Account')
|
||||||
},
|
},
|
||||||
[upsertAccount, queryClient],
|
[upsertAccount, queryClient, clearCurrentAccount],
|
||||||
)
|
)
|
||||||
|
|
||||||
const login = React.useCallback<ApiContext['login']>(
|
const login = React.useCallback<ApiContext['login']>(
|
||||||
@@ -267,9 +294,13 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
agent.setPersistSessionHandler(
|
agent.setPersistSessionHandler(
|
||||||
createPersistSessionHandler(account, ({expired, refreshedAccount}) => {
|
createPersistSessionHandler(
|
||||||
upsertAccount(refreshedAccount, expired)
|
account,
|
||||||
}),
|
({expired, refreshedAccount}) => {
|
||||||
|
upsertAccount(refreshedAccount, expired)
|
||||||
|
},
|
||||||
|
{networkErrorCallback: clearCurrentAccount},
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
__globalAgent = agent
|
__globalAgent = agent
|
||||||
@@ -287,23 +318,9 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
|
|||||||
|
|
||||||
track('Sign In', {resumedSession: false})
|
track('Sign In', {resumedSession: false})
|
||||||
},
|
},
|
||||||
[upsertAccount, queryClient],
|
[upsertAccount, queryClient, clearCurrentAccount],
|
||||||
)
|
)
|
||||||
|
|
||||||
const clearCurrentAccount = React.useCallback(() => {
|
|
||||||
logger.debug(
|
|
||||||
`session: clear current account`,
|
|
||||||
{},
|
|
||||||
logger.DebugContext.session,
|
|
||||||
)
|
|
||||||
__globalAgent = PUBLIC_BSKY_AGENT
|
|
||||||
queryClient.clear()
|
|
||||||
setStateAndPersist(s => ({
|
|
||||||
...s,
|
|
||||||
currentAccount: undefined,
|
|
||||||
}))
|
|
||||||
}, [setStateAndPersist, queryClient])
|
|
||||||
|
|
||||||
const logout = React.useCallback<ApiContext['logout']>(async () => {
|
const logout = React.useCallback<ApiContext['logout']>(async () => {
|
||||||
clearCurrentAccount()
|
clearCurrentAccount()
|
||||||
logger.debug(`session: logout`, {}, logger.DebugContext.session)
|
logger.debug(`session: logout`, {}, logger.DebugContext.session)
|
||||||
@@ -337,6 +354,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
|
|||||||
({expired, refreshedAccount}) => {
|
({expired, refreshedAccount}) => {
|
||||||
upsertAccount(refreshedAccount, expired)
|
upsertAccount(refreshedAccount, expired)
|
||||||
},
|
},
|
||||||
|
{networkErrorCallback: clearCurrentAccount},
|
||||||
),
|
),
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -437,7 +455,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[upsertAccount, queryClient],
|
[upsertAccount, queryClient, clearCurrentAccount],
|
||||||
)
|
)
|
||||||
|
|
||||||
const resumeSession = React.useCallback<ApiContext['resumeSession']>(
|
const resumeSession = React.useCallback<ApiContext['resumeSession']>(
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ export interface ComposerOptsPostRef {
|
|||||||
displayName?: string
|
displayName?: string
|
||||||
avatar?: string
|
avatar?: string
|
||||||
}
|
}
|
||||||
|
embed?: AppBskyEmbedRecord.ViewRecord['embed']
|
||||||
}
|
}
|
||||||
export interface ComposerOptsQuote {
|
export interface ComposerOptsQuote {
|
||||||
uri: string
|
uri: string
|
||||||
@@ -30,6 +31,7 @@ export interface ComposerOpts {
|
|||||||
onPost?: () => void
|
onPost?: () => void
|
||||||
quote?: ComposerOptsQuote
|
quote?: ComposerOptsQuote
|
||||||
mention?: string // handle of user to mention
|
mention?: string // handle of user to mention
|
||||||
|
openPicker?: (pos: DOMRect | undefined) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
type StateContext = ComposerOpts | undefined
|
type StateContext = ComposerOpts | undefined
|
||||||
|
|||||||
@@ -0,0 +1,204 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import {Pressable, Text, PressableProps, TextProps} from 'react-native'
|
||||||
|
import * as tokens from '#/alf/tokens'
|
||||||
|
import {atoms} from '#/alf'
|
||||||
|
|
||||||
|
export type ButtonType =
|
||||||
|
| 'primary'
|
||||||
|
| 'secondary'
|
||||||
|
| 'tertiary'
|
||||||
|
| 'positive'
|
||||||
|
| 'negative'
|
||||||
|
export type ButtonSize = 'small' | 'large'
|
||||||
|
|
||||||
|
export type VariantProps = {
|
||||||
|
type?: ButtonType
|
||||||
|
size?: ButtonSize
|
||||||
|
}
|
||||||
|
type ButtonState = {
|
||||||
|
pressed: boolean
|
||||||
|
hovered: boolean
|
||||||
|
focused: boolean
|
||||||
|
}
|
||||||
|
export type ButtonProps = Omit<PressableProps, 'children'> &
|
||||||
|
VariantProps & {
|
||||||
|
children:
|
||||||
|
| ((props: {
|
||||||
|
state: ButtonState
|
||||||
|
type?: ButtonType
|
||||||
|
size?: ButtonSize
|
||||||
|
}) => React.ReactNode)
|
||||||
|
| React.ReactNode
|
||||||
|
| string
|
||||||
|
}
|
||||||
|
export type ButtonTextProps = TextProps & VariantProps
|
||||||
|
|
||||||
|
export function Button({children, style, type, size, ...rest}: ButtonProps) {
|
||||||
|
const {baseStyles, hoverStyles} = React.useMemo(() => {
|
||||||
|
const baseStyles = []
|
||||||
|
const hoverStyles = []
|
||||||
|
|
||||||
|
switch (type) {
|
||||||
|
case 'primary':
|
||||||
|
baseStyles.push({
|
||||||
|
backgroundColor: tokens.color.blue_500,
|
||||||
|
})
|
||||||
|
break
|
||||||
|
case 'secondary':
|
||||||
|
baseStyles.push({
|
||||||
|
backgroundColor: tokens.color.gray_200,
|
||||||
|
})
|
||||||
|
hoverStyles.push({
|
||||||
|
backgroundColor: tokens.color.gray_100,
|
||||||
|
})
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (size) {
|
||||||
|
case 'large':
|
||||||
|
baseStyles.push(
|
||||||
|
atoms.py_md,
|
||||||
|
atoms.px_xl,
|
||||||
|
atoms.rounded_md,
|
||||||
|
atoms.gap_sm,
|
||||||
|
)
|
||||||
|
break
|
||||||
|
case 'small':
|
||||||
|
baseStyles.push(
|
||||||
|
atoms.py_sm,
|
||||||
|
atoms.px_md,
|
||||||
|
atoms.rounded_sm,
|
||||||
|
atoms.gap_xs,
|
||||||
|
)
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
baseStyles,
|
||||||
|
hoverStyles,
|
||||||
|
}
|
||||||
|
}, [type, size])
|
||||||
|
|
||||||
|
const [state, setState] = React.useState({
|
||||||
|
pressed: false,
|
||||||
|
hovered: false,
|
||||||
|
focused: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
const onPressIn = React.useCallback(() => {
|
||||||
|
setState(s => ({
|
||||||
|
...s,
|
||||||
|
pressed: true,
|
||||||
|
}))
|
||||||
|
}, [setState])
|
||||||
|
const onPressOut = React.useCallback(() => {
|
||||||
|
setState(s => ({
|
||||||
|
...s,
|
||||||
|
pressed: false,
|
||||||
|
}))
|
||||||
|
}, [setState])
|
||||||
|
const onHoverIn = React.useCallback(() => {
|
||||||
|
setState(s => ({
|
||||||
|
...s,
|
||||||
|
hovered: true,
|
||||||
|
}))
|
||||||
|
}, [setState])
|
||||||
|
const onHoverOut = React.useCallback(() => {
|
||||||
|
setState(s => ({
|
||||||
|
...s,
|
||||||
|
hovered: false,
|
||||||
|
}))
|
||||||
|
}, [setState])
|
||||||
|
const onFocus = React.useCallback(() => {
|
||||||
|
setState(s => ({
|
||||||
|
...s,
|
||||||
|
focused: true,
|
||||||
|
}))
|
||||||
|
}, [setState])
|
||||||
|
const onBlur = React.useCallback(() => {
|
||||||
|
setState(s => ({
|
||||||
|
...s,
|
||||||
|
focused: false,
|
||||||
|
}))
|
||||||
|
}, [setState])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Pressable
|
||||||
|
{...rest}
|
||||||
|
style={state => [
|
||||||
|
atoms.flex_row,
|
||||||
|
atoms.align_center,
|
||||||
|
...baseStyles,
|
||||||
|
...(state.hovered ? hoverStyles : []),
|
||||||
|
typeof style === 'function' ? style(state) : style,
|
||||||
|
]}
|
||||||
|
onPressIn={onPressIn}
|
||||||
|
onPressOut={onPressOut}
|
||||||
|
onHoverIn={onHoverIn}
|
||||||
|
onHoverOut={onHoverOut}
|
||||||
|
onFocus={onFocus}
|
||||||
|
onBlur={onBlur}>
|
||||||
|
{typeof children === 'string' ? (
|
||||||
|
<ButtonText type={type} size={size}>
|
||||||
|
{children}
|
||||||
|
</ButtonText>
|
||||||
|
) : typeof children === 'function' ? (
|
||||||
|
children({state, type, size})
|
||||||
|
) : (
|
||||||
|
children
|
||||||
|
)}
|
||||||
|
</Pressable>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ButtonText({
|
||||||
|
children,
|
||||||
|
style,
|
||||||
|
type,
|
||||||
|
size,
|
||||||
|
...rest
|
||||||
|
}: ButtonTextProps) {
|
||||||
|
const textStyles = React.useMemo(() => {
|
||||||
|
const base = []
|
||||||
|
|
||||||
|
switch (type) {
|
||||||
|
case 'primary':
|
||||||
|
base.push({color: tokens.color.white})
|
||||||
|
break
|
||||||
|
case 'secondary':
|
||||||
|
base.push({
|
||||||
|
color: tokens.color.gray_700,
|
||||||
|
})
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (size) {
|
||||||
|
case 'small':
|
||||||
|
base.push(atoms.text_sm, {paddingBottom: 1})
|
||||||
|
break
|
||||||
|
case 'large':
|
||||||
|
base.push(atoms.text_md, {paddingBottom: 1})
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
|
||||||
|
return base
|
||||||
|
}, [type, size])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Text
|
||||||
|
{...rest}
|
||||||
|
style={[
|
||||||
|
atoms.flex_1,
|
||||||
|
atoms.font_semibold,
|
||||||
|
atoms.text_center,
|
||||||
|
...textStyles,
|
||||||
|
style,
|
||||||
|
]}>
|
||||||
|
{children}
|
||||||
|
</Text>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import {Text as RNText, TextProps} from 'react-native'
|
||||||
|
import {useTheme, atoms, web} from '#/alf'
|
||||||
|
|
||||||
|
export function Text({style, ...rest}: TextProps) {
|
||||||
|
const t = useTheme()
|
||||||
|
return <RNText style={[atoms.text_sm, t.atoms.text, style]} {...rest} />
|
||||||
|
}
|
||||||
|
|
||||||
|
export function H1({style, ...rest}: TextProps) {
|
||||||
|
const t = useTheme()
|
||||||
|
const attr =
|
||||||
|
web({
|
||||||
|
role: 'heading',
|
||||||
|
'aria-level': 1,
|
||||||
|
}) || {}
|
||||||
|
return (
|
||||||
|
<RNText
|
||||||
|
{...attr}
|
||||||
|
{...rest}
|
||||||
|
style={[atoms.text_xl, atoms.font_bold, t.atoms.text, style]}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function H2({style, ...rest}: TextProps) {
|
||||||
|
const t = useTheme()
|
||||||
|
const attr =
|
||||||
|
web({
|
||||||
|
role: 'heading',
|
||||||
|
'aria-level': 2,
|
||||||
|
}) || {}
|
||||||
|
return (
|
||||||
|
<RNText
|
||||||
|
{...attr}
|
||||||
|
{...rest}
|
||||||
|
style={[atoms.text_lg, atoms.font_bold, t.atoms.text, style]}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function H3({style, ...rest}: TextProps) {
|
||||||
|
const t = useTheme()
|
||||||
|
const attr =
|
||||||
|
web({
|
||||||
|
role: 'heading',
|
||||||
|
'aria-level': 3,
|
||||||
|
}) || {}
|
||||||
|
return (
|
||||||
|
<RNText
|
||||||
|
{...attr}
|
||||||
|
{...rest}
|
||||||
|
style={[atoms.text_md, atoms.font_bold, t.atoms.text, style]}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function H4({style, ...rest}: TextProps) {
|
||||||
|
const t = useTheme()
|
||||||
|
const attr =
|
||||||
|
web({
|
||||||
|
role: 'heading',
|
||||||
|
'aria-level': 4,
|
||||||
|
}) || {}
|
||||||
|
return (
|
||||||
|
<RNText
|
||||||
|
{...attr}
|
||||||
|
{...rest}
|
||||||
|
style={[atoms.text_sm, atoms.font_bold, t.atoms.text, style]}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function H5({style, ...rest}: TextProps) {
|
||||||
|
const t = useTheme()
|
||||||
|
const attr =
|
||||||
|
web({
|
||||||
|
role: 'heading',
|
||||||
|
'aria-level': 5,
|
||||||
|
}) || {}
|
||||||
|
return (
|
||||||
|
<RNText
|
||||||
|
{...attr}
|
||||||
|
{...rest}
|
||||||
|
style={[atoms.text_xs, atoms.font_bold, t.atoms.text, style]}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function H6({style, ...rest}: TextProps) {
|
||||||
|
const t = useTheme()
|
||||||
|
const attr =
|
||||||
|
web({
|
||||||
|
role: 'heading',
|
||||||
|
'aria-level': 6,
|
||||||
|
}) || {}
|
||||||
|
return (
|
||||||
|
<RNText
|
||||||
|
{...attr}
|
||||||
|
{...rest}
|
||||||
|
style={[atoms.text_xxs, atoms.font_bold, t.atoms.text, style]}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -2,7 +2,7 @@ import React from 'react'
|
|||||||
import {View, Pressable} from 'react-native'
|
import {View, Pressable} from 'react-native'
|
||||||
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
||||||
import {useLingui} from '@lingui/react'
|
import {useLingui} from '@lingui/react'
|
||||||
import {msg} from '@lingui/macro'
|
import {Trans, msg} from '@lingui/macro'
|
||||||
import {useNavigation} from '@react-navigation/native'
|
import {useNavigation} from '@react-navigation/native'
|
||||||
|
|
||||||
import {isIOS, isNative} from 'platform/detection'
|
import {isIOS, isNative} from 'platform/detection'
|
||||||
@@ -119,7 +119,7 @@ export function LoggedOut({onDismiss}: {onDismiss?: () => void}) {
|
|||||||
}}
|
}}
|
||||||
onPress={onPressSearch}>
|
onPress={onPressSearch}>
|
||||||
<Text type="lg-bold" style={[pal.text]}>
|
<Text type="lg-bold" style={[pal.text]}>
|
||||||
Search{' '}
|
<Trans>Search</Trans>{' '}
|
||||||
</Text>
|
</Text>
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon
|
||||||
icon="search"
|
icon="search"
|
||||||
|
|||||||