Merge remote-tracking branch 'origin/main' into eric/nova
* origin/main: (67 commits) Localize options in "Thread Preferences" screen (#2373) Bump ios build number and android version code Remove the KeyboardAvoidingView in account creation (close #2333) (#2366) 1.62 Create account tweaks (#2365) Fix sizing of the leftnav new post btn (#2248) Use memory caching for android lightbox (#2354) Disable page-transition animations on android (#2352) Disable BlurView on android (#2351) Handle birth dates as UTC, handle locale formatting (#2363) Fix desktop styles a bit Disable spanish translation until it's more thoroughly reviewed (#2362) minor search screen ux improvements (#2264) Temporarily disable the german translation (#2360) Web dropdowns (#2358) Mark more text as translatable (#2284) Remove patched color scheme code (#2340) support multiple og:image tags (#2305) Fix missing avatar moderation in replies (#2325) Fixes to feed load triggers (#2323) ...
@@ -1,6 +1,5 @@
|
|||||||
import {RichText} from '@atproto/api'
|
import {RichText} from '@atproto/api'
|
||||||
import {
|
import {
|
||||||
getYoutubeVideoId,
|
|
||||||
makeRecordUri,
|
makeRecordUri,
|
||||||
toNiceDomain,
|
toNiceDomain,
|
||||||
toShortUrl,
|
toShortUrl,
|
||||||
@@ -12,6 +11,7 @@ import {detectLinkables} from '../../src/lib/strings/rich-text-detection'
|
|||||||
import {shortenLinks} from '../../src/lib/strings/rich-text-manip'
|
import {shortenLinks} from '../../src/lib/strings/rich-text-manip'
|
||||||
import {makeValidHandle, createFullHandle} from '../../src/lib/strings/handles'
|
import {makeValidHandle, createFullHandle} from '../../src/lib/strings/handles'
|
||||||
import {cleanError} from '../../src/lib/strings/errors'
|
import {cleanError} from '../../src/lib/strings/errors'
|
||||||
|
import {parseEmbedPlayerFromUrl} from 'lib/strings/embed-player'
|
||||||
|
|
||||||
describe('detectLinkables', () => {
|
describe('detectLinkables', () => {
|
||||||
const inputs = [
|
const inputs = [
|
||||||
@@ -335,32 +335,6 @@ describe('toShareUrl', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('getYoutubeVideoId', () => {
|
|
||||||
it(' should return undefined for invalid youtube links', () => {
|
|
||||||
expect(getYoutubeVideoId('')).toBeUndefined()
|
|
||||||
expect(getYoutubeVideoId('https://www.google.com')).toBeUndefined()
|
|
||||||
expect(getYoutubeVideoId('https://www.youtube.com')).toBeUndefined()
|
|
||||||
expect(
|
|
||||||
getYoutubeVideoId('https://www.youtube.com/channelName'),
|
|
||||||
).toBeUndefined()
|
|
||||||
expect(
|
|
||||||
getYoutubeVideoId('https://www.youtube.com/channel/channelName'),
|
|
||||||
).toBeUndefined()
|
|
||||||
})
|
|
||||||
|
|
||||||
it('getYoutubeVideoId should return video id for valid youtube links', () => {
|
|
||||||
expect(getYoutubeVideoId('https://www.youtube.com/watch?v=videoId')).toBe(
|
|
||||||
'videoId',
|
|
||||||
)
|
|
||||||
expect(
|
|
||||||
getYoutubeVideoId(
|
|
||||||
'https://www.youtube.com/watch?v=videoId&feature=share',
|
|
||||||
),
|
|
||||||
).toBe('videoId')
|
|
||||||
expect(getYoutubeVideoId('https://youtu.be/videoId')).toBe('videoId')
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('shortenLinks', () => {
|
describe('shortenLinks', () => {
|
||||||
const inputs = [
|
const inputs = [
|
||||||
'start https://middle.com/foo/bar?baz=bux#hash end',
|
'start https://middle.com/foo/bar?baz=bux#hash end',
|
||||||
@@ -396,6 +370,7 @@ describe('shortenLinks', () => {
|
|||||||
],
|
],
|
||||||
],
|
],
|
||||||
]
|
]
|
||||||
|
|
||||||
it('correctly shortens rich text while preserving facet URIs', () => {
|
it('correctly shortens rich text while preserving facet URIs', () => {
|
||||||
for (let i = 0; i < inputs.length; i++) {
|
for (let i = 0; i < inputs.length; i++) {
|
||||||
const input = inputs[i]
|
const input = inputs[i]
|
||||||
@@ -410,3 +385,141 @@ describe('shortenLinks', () => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('parseEmbedPlayerFromUrl', () => {
|
||||||
|
const inputs = [
|
||||||
|
'https://youtu.be/videoId',
|
||||||
|
'https://www.youtube.com/watch?v=videoId',
|
||||||
|
'https://www.youtube.com/watch?v=videoId&feature=share',
|
||||||
|
'https://youtube.com/watch?v=videoId',
|
||||||
|
'https://youtube.com/watch?v=videoId&feature=share',
|
||||||
|
'https://youtube.com/shorts/videoId',
|
||||||
|
|
||||||
|
'https://youtube.com/shorts/',
|
||||||
|
'https://youtube.com/',
|
||||||
|
'https://youtube.com/random',
|
||||||
|
|
||||||
|
'https://twitch.tv/channelName',
|
||||||
|
'https://www.twitch.tv/channelName',
|
||||||
|
|
||||||
|
'https://open.spotify.com/playlist/playlistId',
|
||||||
|
'https://open.spotify.com/playlist/playlistId?param=value',
|
||||||
|
|
||||||
|
'https://open.spotify.com/track/songId',
|
||||||
|
'https://open.spotify.com/track/songId?param=value',
|
||||||
|
|
||||||
|
'https://open.spotify.com/album/albumId',
|
||||||
|
'https://open.spotify.com/album/albumId?param=value',
|
||||||
|
|
||||||
|
'https://soundcloud.com/user/track',
|
||||||
|
'https://soundcloud.com/user/sets/set',
|
||||||
|
'https://soundcloud.com/user/',
|
||||||
|
]
|
||||||
|
|
||||||
|
const outputs = [
|
||||||
|
{
|
||||||
|
type: 'youtube_video',
|
||||||
|
videoId: 'videoId',
|
||||||
|
playerUri: 'https://www.youtube.com/embed/videoId?autoplay=1',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'youtube_video',
|
||||||
|
videoId: 'videoId',
|
||||||
|
playerUri: 'https://www.youtube.com/embed/videoId?autoplay=1',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'youtube_video',
|
||||||
|
videoId: 'videoId',
|
||||||
|
playerUri: 'https://www.youtube.com/embed/videoId?autoplay=1',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'youtube_video',
|
||||||
|
videoId: 'videoId',
|
||||||
|
playerUri: 'https://www.youtube.com/embed/videoId?autoplay=1',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'youtube_video',
|
||||||
|
videoId: 'videoId',
|
||||||
|
playerUri: 'https://www.youtube.com/embed/videoId?autoplay=1',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'youtube_video',
|
||||||
|
videoId: 'videoId',
|
||||||
|
playerUri: 'https://www.youtube.com/embed/videoId?autoplay=1',
|
||||||
|
},
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
|
||||||
|
{
|
||||||
|
type: 'twitch_live',
|
||||||
|
channelId: 'channelName',
|
||||||
|
playerUri: `https://player.twitch.tv/?volume=0.5&!muted&autoplay&channel=channelName&parent=localhost`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'twitch_live',
|
||||||
|
channelId: 'channelName',
|
||||||
|
playerUri: `https://player.twitch.tv/?volume=0.5&!muted&autoplay&channel=channelName&parent=localhost`,
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
type: 'spotify_playlist',
|
||||||
|
playlistId: 'playlistId',
|
||||||
|
playerUri: `https://open.spotify.com/embed/playlist/playlistId`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'spotify_playlist',
|
||||||
|
playlistId: 'playlistId',
|
||||||
|
playerUri: `https://open.spotify.com/embed/playlist/playlistId`,
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
type: 'spotify_song',
|
||||||
|
songId: 'songId',
|
||||||
|
playerUri: `https://open.spotify.com/embed/track/songId`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'spotify_song',
|
||||||
|
songId: 'songId',
|
||||||
|
playerUri: `https://open.spotify.com/embed/track/songId`,
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
type: 'spotify_album',
|
||||||
|
albumId: 'albumId',
|
||||||
|
playerUri: `https://open.spotify.com/embed/album/albumId`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'spotify_album',
|
||||||
|
albumId: 'albumId',
|
||||||
|
playerUri: `https://open.spotify.com/embed/album/albumId`,
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
type: 'soundcloud_track',
|
||||||
|
user: 'user',
|
||||||
|
track: 'track',
|
||||||
|
playerUri: `https://w.soundcloud.com/player/?url=https://soundcloud.com/user/track&auto_play=true&visual=false&hide_related=true`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'soundcloud_set',
|
||||||
|
user: 'user',
|
||||||
|
set: 'set',
|
||||||
|
playerUri: `https://w.soundcloud.com/player/?url=https://soundcloud.com/user/sets/set&auto_play=true&visual=false&hide_related=true`,
|
||||||
|
},
|
||||||
|
undefined,
|
||||||
|
]
|
||||||
|
|
||||||
|
it('correctly grabs the correct id from uri', () => {
|
||||||
|
for (let i = 0; i < inputs.length; i++) {
|
||||||
|
const input = inputs[i]
|
||||||
|
const output = outputs[i]
|
||||||
|
|
||||||
|
const res = parseEmbedPlayerFromUrl(input)
|
||||||
|
|
||||||
|
console.log(input)
|
||||||
|
|
||||||
|
expect(res).toEqual(output)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|||||||
@@ -9,12 +9,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 = '4'
|
const IOS_BUILD_NUMBER = '2'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Android build number. Must be incremented for each release.
|
* Android build number. Must be incremented for each release.
|
||||||
*/
|
*/
|
||||||
const ANDROID_VERSION_CODE = 50
|
const ANDROID_VERSION_CODE = 53
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Uses built-in Expo env vars
|
* Uses built-in Expo env vars
|
||||||
@@ -110,6 +110,9 @@ module.exports = function () {
|
|||||||
[
|
[
|
||||||
'expo-build-properties',
|
'expo-build-properties',
|
||||||
{
|
{
|
||||||
|
ios: {
|
||||||
|
deploymentTarget: '13.4',
|
||||||
|
},
|
||||||
android: {
|
android: {
|
||||||
compileSdkVersion: 34,
|
compileSdkVersion: 34,
|
||||||
targetSdkVersion: 34,
|
targetSdkVersion: 34,
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 223 KiB After Width: | Height: | Size: 223 KiB |
|
Before Width: | Height: | Size: 452 KiB After Width: | Height: | Size: 452 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',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/xml"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
appbsky "github.com/bluesky-social/indigo/api/bsky"
|
appbsky "github.com/bluesky-social/indigo/api/bsky"
|
||||||
"github.com/bluesky-social/indigo/atproto/syntax"
|
"github.com/bluesky-social/indigo/atproto/syntax"
|
||||||
@@ -10,6 +13,12 @@ import (
|
|||||||
"github.com/labstack/echo/v4"
|
"github.com/labstack/echo/v4"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type ItemGUID struct {
|
||||||
|
XMLName xml.Name `xml:"guid"`
|
||||||
|
Value string `xml:",chardata"`
|
||||||
|
IsPerma bool `xml:"isPermaLink,attr"`
|
||||||
|
}
|
||||||
|
|
||||||
// We don't actually populate the title for "posts".
|
// We don't actually populate the title for "posts".
|
||||||
// Some background: https://book.micro.blog/rss-for-microblogs/
|
// Some background: https://book.micro.blog/rss-for-microblogs/
|
||||||
type Item struct {
|
type Item struct {
|
||||||
@@ -17,8 +26,7 @@ type Item struct {
|
|||||||
Link string `xml:"link,omitempty"`
|
Link string `xml:"link,omitempty"`
|
||||||
Description string `xml:"description,omitempty"`
|
Description string `xml:"description,omitempty"`
|
||||||
PubDate string `xml:"pubDate,omitempty"`
|
PubDate string `xml:"pubDate,omitempty"`
|
||||||
Author string `xml:"author,omitempty"`
|
GUID ItemGUID
|
||||||
GUID string `xml:"guid,omitempty"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type rss struct {
|
type rss struct {
|
||||||
@@ -32,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 {
|
if err != nil {
|
||||||
return echo.NewHTTPError(400, fmt.Sprintf("not a valid DID: %s", didParam))
|
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 {
|
||||||
|
return echo.NewHTTPError(400, fmt.Sprintf("not a valid DID: %s", identParam))
|
||||||
}
|
}
|
||||||
|
|
||||||
// check that public view is Ok
|
// check that public view is Ok
|
||||||
@@ -71,12 +101,19 @@ func (srv *Server) WebProfileRSS(c echo.Context) error {
|
|||||||
if rec.Reply != nil {
|
if rec.Reply != nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
pubDate := ""
|
||||||
|
createdAt, err := syntax.ParseDatetimeLenient(rec.CreatedAt)
|
||||||
|
if nil == err {
|
||||||
|
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: rec.CreatedAt,
|
PubDate: pubDate,
|
||||||
Author: "@" + pv.Handle,
|
GUID: ItemGUID{
|
||||||
GUID: aturi.String(),
|
Value: aturi.String(),
|
||||||
|
IsPerma: false,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -91,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,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -210,7 +210,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 +336,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 +374,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 |
@@ -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 %}
|
||||||
|
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ import { Text } from "react-native";
|
|||||||
const text = "Hello World";
|
const text = "Hello World";
|
||||||
<Text accessibilityLabel="Label is here">{text}</Text>
|
<Text accessibilityLabel="Label is here">{text}</Text>
|
||||||
```
|
```
|
||||||
In this case, you cannot use the `useLingui()` hook:
|
In this case, you can use the `useLingui()` hook:
|
||||||
```jsx
|
```jsx
|
||||||
import { msg } from "@lingui/macro";
|
import { msg } from "@lingui/macro";
|
||||||
import { useLingui } from "@lingui/react";
|
import { useLingui } from "@lingui/react";
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
/** @type {import('@lingui/conf').LinguiConfig} */
|
/** @type {import('@lingui/conf').LinguiConfig} */
|
||||||
module.exports = {
|
module.exports = {
|
||||||
locales: ['en', 'hi', 'ja'],
|
locales: ['en', 'hi', 'ja', 'fr', 'de', 'es'],
|
||||||
catalogs: [
|
catalogs: [
|
||||||
{
|
{
|
||||||
path: '<rootDir>/src/locale/locales/{locale}/messages',
|
path: '<rootDir>/src/locale/locales/{locale}/messages',
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
{
|
{
|
||||||
"name": "bsky.app",
|
"name": "bsky.app",
|
||||||
"version": "1.60.0",
|
"version": "1.62.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"prepare": "is-ci || husky install",
|
"prepare": "is-ci || husky install",
|
||||||
"postinstall": "patch-package",
|
"postinstall": "patch-package && yarn intl:compile",
|
||||||
"prebuild": "expo prebuild --clean",
|
"prebuild": "expo prebuild --clean",
|
||||||
"android": "expo run:android",
|
"android": "expo run:android",
|
||||||
"ios": "expo run:ios",
|
"ios": "expo run:ios",
|
||||||
@@ -32,7 +35,8 @@
|
|||||||
"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.7.4",
|
||||||
@@ -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,31 +140,34 @@
|
|||||||
"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": "^3.2.0",
|
"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-splash-screen": "^3.3.0",
|
||||||
"react-native-svg": "13.9.0",
|
"react-native-svg": "14.0.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-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",
|
||||||
@@ -175,10 +183,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",
|
||||||
@@ -199,11 +210,12 @@
|
|||||||
"@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",
|
||||||
|
"babel-preset-expo": "^10.0.0",
|
||||||
"detox": "^20.13.0",
|
"detox": "^20.13.0",
|
||||||
"eslint": "^8.19.0",
|
"eslint": "^8.19.0",
|
||||||
"eslint-plugin-detox": "^1.0.0",
|
"eslint-plugin-detox": "^1.0.0",
|
||||||
@@ -214,8 +226,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",
|
||||||
@@ -225,7 +237,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 }],
|
|
||||||
@@ -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
|
|
||||||
@@ -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'
|
||||||
@@ -286,6 +286,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,
|
||||||
@@ -307,6 +308,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,
|
||||||
@@ -324,6 +326,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,
|
||||||
@@ -345,6 +348,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,
|
||||||
@@ -366,6 +370,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,
|
||||||
|
|||||||
@@ -40,8 +40,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>) {
|
||||||
@@ -49,6 +47,7 @@ export function Splash(props: React.PropsWithChildren<Props>) {
|
|||||||
const intro = useSharedValue(0)
|
const intro = useSharedValue(0)
|
||||||
const outroLogo = useSharedValue(0)
|
const outroLogo = useSharedValue(0)
|
||||||
const outroApp = useSharedValue(0)
|
const outroApp = 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 isReady = props.isReady && isImageLoaded
|
||||||
@@ -62,8 +61,8 @@ export function Splash(props: React.PropsWithChildren<Props>) {
|
|||||||
{
|
{
|
||||||
scale: interpolate(
|
scale: interpolate(
|
||||||
outroLogo.value,
|
outroLogo.value,
|
||||||
[0, 0.06, 0.08, 1],
|
[0, 0.08, 1],
|
||||||
[1, 0.8, 0.8, 400],
|
[1, 0.8, 400],
|
||||||
'clamp',
|
'clamp',
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
@@ -79,7 +78,12 @@ export function Splash(props: React.PropsWithChildren<Props>) {
|
|||||||
scale: interpolate(outroApp.value, [0, 1], [1.1, 1], 'clamp'),
|
scale: interpolate(outroApp.value, [0, 1], [1.1, 1], 'clamp'),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
opacity: interpolate(outroApp.value, [0, 0.9, 1], [0, 1, 1], 'clamp'),
|
opacity: interpolate(
|
||||||
|
outroAppOpacity.value,
|
||||||
|
[0, 0.08, 0.15, 1],
|
||||||
|
[0, 0, 1, 1],
|
||||||
|
'clamp',
|
||||||
|
),
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -92,29 +96,30 @@ export function Splash(props: React.PropsWithChildren<Props>) {
|
|||||||
|
|
||||||
intro.value = withTiming(
|
intro.value = withTiming(
|
||||||
1,
|
1,
|
||||||
{duration: 200, easing: Easing.out(Easing.cubic)},
|
{duration: 400, easing: Easing.out(Easing.cubic)},
|
||||||
async () => {
|
async () => {
|
||||||
// set these values to check animation at specific point
|
// set these values to check animation at specific point
|
||||||
// outroLogo.value = 0.1
|
// outroLogo.value = 0.1
|
||||||
// outroApp.value = 0.1
|
// outroApp.value = 0.1
|
||||||
outroLogo.value = withTiming(
|
outroLogo.value = withTiming(
|
||||||
1,
|
1,
|
||||||
{duration: 1000, easing: Easing.in(Easing.cubic)},
|
{duration: 1200, easing: Easing.in(Easing.cubic)},
|
||||||
() => {
|
|
||||||
runOnJS(onFinish)()
|
|
||||||
},
|
|
||||||
)
|
|
||||||
outroApp.value = withTiming(
|
|
||||||
1,
|
|
||||||
{duration: 1000, easing: Easing.inOut(Easing.cubic)},
|
|
||||||
() => {
|
() => {
|
||||||
runOnJS(onFinish)()
|
runOnJS(onFinish)()
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
outroApp.value = withTiming(1, {
|
||||||
|
duration: 1200,
|
||||||
|
easing: Easing.inOut(Easing.cubic),
|
||||||
|
})
|
||||||
|
outroAppOpacity.value = withTiming(1, {
|
||||||
|
duration: 1200,
|
||||||
|
easing: Easing.in(Easing.cubic),
|
||||||
|
})
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}, [onFinish, intro, outroLogo, outroApp, isReady])
|
}, [onFinish, intro, outroLogo, outroApp, outroAppOpacity, isReady])
|
||||||
|
|
||||||
const onLoadEnd = useCallback(() => {
|
const onLoadEnd = useCallback(() => {
|
||||||
setIsImageLoaded(true)
|
setIsImageLoaded(true)
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
|
import {useColorScheme} from 'react-native'
|
||||||
|
|
||||||
import * as persisted from '#/state/persisted'
|
import * as persisted from '#/state/persisted'
|
||||||
import {useColorScheme_FIXED} from '#/lib/hooks/useColorScheme_FIXED'
|
|
||||||
|
|
||||||
export function useColorModeTheme(
|
export function useColorModeTheme(
|
||||||
theme: persisted.Schema['colorMode'],
|
theme: persisted.Schema['colorMode'],
|
||||||
): 'light' | 'dark' {
|
): 'light' | 'dark' {
|
||||||
const colorScheme = useColorScheme_FIXED()
|
const colorScheme = useColorScheme()
|
||||||
return (theme === 'system' ? colorScheme : theme) || 'light'
|
return (theme === 'system' ? colorScheme : theme) || 'light'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,11 @@
|
|||||||
import React, {ReactNode, createContext, useContext} from 'react'
|
import React, {ReactNode, createContext, useContext} from 'react'
|
||||||
import {TextStyle, ViewStyle, ColorSchemeName} from 'react-native'
|
import {
|
||||||
|
TextStyle,
|
||||||
|
useColorScheme,
|
||||||
|
ViewStyle,
|
||||||
|
ColorSchemeName,
|
||||||
|
} from 'react-native'
|
||||||
import {darkTheme, defaultTheme} from './themes'
|
import {darkTheme, defaultTheme} from './themes'
|
||||||
import {useColorScheme_FIXED} from '#/lib/hooks/useColorScheme_FIXED'
|
|
||||||
|
|
||||||
export type ColorScheme = 'light' | 'dark'
|
export type ColorScheme = 'light' | 'dark'
|
||||||
|
|
||||||
@@ -95,7 +99,7 @@ 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 (
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ interface TrackPropertiesMap {
|
|||||||
'Sign In': {resumedSession: boolean} // CAN BE SERVER
|
'Sign In': {resumedSession: boolean} // CAN BE SERVER
|
||||||
'Create Account': {} // CAN BE SERVER
|
'Create Account': {} // CAN BE SERVER
|
||||||
'Try Create Account': {}
|
'Try Create Account': {}
|
||||||
'Create Account Successfully': {}
|
|
||||||
'Signin:PressedForgotPassword': {}
|
'Signin:PressedForgotPassword': {}
|
||||||
'Signin:PressedSelectService': {}
|
'Signin:PressedSelectService': {}
|
||||||
// COMPOSER / CREATE POST events
|
// COMPOSER / CREATE POST events
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -1,34 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
import {
|
|
||||||
AppState,
|
|
||||||
useColorScheme as useColorScheme_BUGGY,
|
|
||||||
ColorSchemeName,
|
|
||||||
} from 'react-native'
|
|
||||||
|
|
||||||
import {isWeb} from '#/platform/detection'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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
|
|
||||||
*/
|
|
||||||
export 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
|
|
||||||
}
|
|
||||||
@@ -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}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,152 @@
|
|||||||
|
import {Platform} from 'react-native'
|
||||||
|
|
||||||
|
export type EmbedPlayerParams =
|
||||||
|
| {type: 'youtube_video'; videoId: string; playerUri: string}
|
||||||
|
| {type: 'twitch_live'; channelId: string; playerUri: string}
|
||||||
|
| {type: 'spotify_album'; albumId: string; playerUri: string}
|
||||||
|
| {
|
||||||
|
type: 'spotify_playlist'
|
||||||
|
playlistId: string
|
||||||
|
playerUri: string
|
||||||
|
}
|
||||||
|
| {type: 'spotify_song'; songId: string; playerUri: string}
|
||||||
|
| {type: 'soundcloud_track'; user: string; track: string; playerUri: string}
|
||||||
|
| {type: 'soundcloud_set'; user: string; set: string; playerUri: string}
|
||||||
|
|
||||||
|
export function parseEmbedPlayerFromUrl(
|
||||||
|
url: string,
|
||||||
|
): EmbedPlayerParams | undefined {
|
||||||
|
let urlp
|
||||||
|
try {
|
||||||
|
urlp = new URL(url)
|
||||||
|
} catch (e) {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
// youtube
|
||||||
|
if (urlp.hostname === 'youtu.be') {
|
||||||
|
const videoId = urlp.pathname.split('/')[1]
|
||||||
|
if (videoId) {
|
||||||
|
return {
|
||||||
|
type: 'youtube_video',
|
||||||
|
videoId,
|
||||||
|
playerUri: `https://www.youtube.com/embed/${videoId}?autoplay=1`,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (urlp.hostname === 'www.youtube.com' || urlp.hostname === 'youtube.com') {
|
||||||
|
const [_, page, shortVideoId] = urlp.pathname.split('/')
|
||||||
|
const videoId =
|
||||||
|
page === 'shorts' ? shortVideoId : (urlp.searchParams.get('v') as string)
|
||||||
|
|
||||||
|
if (videoId) {
|
||||||
|
return {
|
||||||
|
type: 'youtube_video',
|
||||||
|
videoId,
|
||||||
|
playerUri: `https://www.youtube.com/embed/${videoId}?autoplay=1`,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// twitch
|
||||||
|
if (urlp.hostname === 'twitch.tv' || urlp.hostname === 'www.twitch.tv') {
|
||||||
|
const parent =
|
||||||
|
Platform.OS === 'web' ? window.location.hostname : 'localhost'
|
||||||
|
|
||||||
|
const parts = urlp.pathname.split('/')
|
||||||
|
if (parts.length === 2 && parts[1]) {
|
||||||
|
return {
|
||||||
|
type: 'twitch_live',
|
||||||
|
channelId: parts[1],
|
||||||
|
playerUri: `https://player.twitch.tv/?volume=0.5&!muted&autoplay&channel=${parts[1]}&parent=${parent}`,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// spotify
|
||||||
|
if (urlp.hostname === 'open.spotify.com') {
|
||||||
|
const [_, type, id] = urlp.pathname.split('/')
|
||||||
|
if (type && id) {
|
||||||
|
if (type === 'playlist') {
|
||||||
|
return {
|
||||||
|
type: 'spotify_playlist',
|
||||||
|
playlistId: id,
|
||||||
|
playerUri: `https://open.spotify.com/embed/playlist/${id}`,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (type === 'album') {
|
||||||
|
return {
|
||||||
|
type: 'spotify_album',
|
||||||
|
albumId: id,
|
||||||
|
playerUri: `https://open.spotify.com/embed/album/${id}`,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (type === 'track') {
|
||||||
|
return {
|
||||||
|
type: 'spotify_song',
|
||||||
|
songId: id,
|
||||||
|
playerUri: `https://open.spotify.com/embed/track/${id}`,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// soundcloud
|
||||||
|
if (
|
||||||
|
urlp.hostname === 'soundcloud.com' ||
|
||||||
|
urlp.hostname === 'www.soundcloud.com'
|
||||||
|
) {
|
||||||
|
const [_, user, trackOrSets, set] = urlp.pathname.split('/')
|
||||||
|
|
||||||
|
if (user && trackOrSets) {
|
||||||
|
if (trackOrSets === 'sets' && set) {
|
||||||
|
return {
|
||||||
|
type: 'soundcloud_set',
|
||||||
|
user,
|
||||||
|
set: set,
|
||||||
|
playerUri: `https://w.soundcloud.com/player/?url=${url}&auto_play=true&visual=false&hide_related=true`,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: 'soundcloud_track',
|
||||||
|
user,
|
||||||
|
track: trackOrSets,
|
||||||
|
playerUri: `https://w.soundcloud.com/player/?url=${url}&auto_play=true&visual=false&hide_related=true`,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getPlayerHeight({
|
||||||
|
type,
|
||||||
|
width,
|
||||||
|
hasThumb,
|
||||||
|
}: {
|
||||||
|
type: EmbedPlayerParams['type']
|
||||||
|
width: number
|
||||||
|
hasThumb: boolean
|
||||||
|
}) {
|
||||||
|
if (!hasThumb) return (width / 16) * 9
|
||||||
|
|
||||||
|
switch (type) {
|
||||||
|
case 'youtube_video':
|
||||||
|
case 'twitch_live':
|
||||||
|
return (width / 16) * 9
|
||||||
|
case 'spotify_album':
|
||||||
|
return 380
|
||||||
|
case 'spotify_playlist':
|
||||||
|
return 360
|
||||||
|
case 'spotify_song':
|
||||||
|
if (width <= 300) {
|
||||||
|
return 180
|
||||||
|
}
|
||||||
|
return 232
|
||||||
|
case 'soundcloud_track':
|
||||||
|
return 165
|
||||||
|
case 'soundcloud_set':
|
||||||
|
return 360
|
||||||
|
default:
|
||||||
|
return width
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -139,35 +139,6 @@ export function feedUriToHref(url: string): string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getYoutubeVideoId(link: string): string | undefined {
|
|
||||||
let url
|
|
||||||
try {
|
|
||||||
url = new URL(link)
|
|
||||||
} catch (e) {
|
|
||||||
return undefined
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
url.hostname !== 'www.youtube.com' &&
|
|
||||||
url.hostname !== 'youtube.com' &&
|
|
||||||
url.hostname !== 'youtu.be'
|
|
||||||
) {
|
|
||||||
return undefined
|
|
||||||
}
|
|
||||||
if (url.hostname === 'youtu.be') {
|
|
||||||
const videoId = url.pathname.split('/')[1]
|
|
||||||
if (!videoId) {
|
|
||||||
return undefined
|
|
||||||
}
|
|
||||||
return videoId
|
|
||||||
}
|
|
||||||
const videoId = url.searchParams.get('v') as string
|
|
||||||
if (!videoId) {
|
|
||||||
return undefined
|
|
||||||
}
|
|
||||||
return videoId
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks if the label in the post text matches the host of the link facet.
|
* Checks if the label in the post text matches the host of the link facet.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -7,6 +7,6 @@ test('sanitizeAppLanguageSetting', () => {
|
|||||||
expect(sanitizeAppLanguageSetting('en')).toBe(AppLanguage.en)
|
expect(sanitizeAppLanguageSetting('en')).toBe(AppLanguage.en)
|
||||||
expect(sanitizeAppLanguageSetting('hi')).toBe(AppLanguage.hi)
|
expect(sanitizeAppLanguageSetting('hi')).toBe(AppLanguage.hi)
|
||||||
expect(sanitizeAppLanguageSetting('foo')).toBe(AppLanguage.en)
|
expect(sanitizeAppLanguageSetting('foo')).toBe(AppLanguage.en)
|
||||||
expect(sanitizeAppLanguageSetting('en,fr')).toBe(AppLanguage.en)
|
expect(sanitizeAppLanguageSetting('en,foo')).toBe(AppLanguage.en)
|
||||||
expect(sanitizeAppLanguageSetting('fr,en')).toBe(AppLanguage.en)
|
expect(sanitizeAppLanguageSetting('foo,en')).toBe(AppLanguage.en)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -114,6 +114,14 @@ export function sanitizeAppLanguageSetting(appLanguage: string): AppLanguage {
|
|||||||
return AppLanguage.hi
|
return AppLanguage.hi
|
||||||
case 'ja':
|
case 'ja':
|
||||||
return AppLanguage.ja
|
return AppLanguage.ja
|
||||||
|
case 'fr':
|
||||||
|
return AppLanguage.fr
|
||||||
|
// DISABLED until this translation is fixed -prf
|
||||||
|
// case 'de':
|
||||||
|
// return AppLanguage.de
|
||||||
|
// DISABLED until this translation is more thoroughly reviewed -prf
|
||||||
|
// case 'es':
|
||||||
|
// return AppLanguage.es
|
||||||
default:
|
default:
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,12 @@ import {useLanguagePrefs} from '#/state/preferences'
|
|||||||
import {messages as messagesEn} from '#/locale/locales/en/messages'
|
import {messages as messagesEn} from '#/locale/locales/en/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'
|
||||||
|
// DISABLED until this translation is fixed -prf
|
||||||
|
// import {messages as messagesDe} from '#/locale/locales/de/messages'
|
||||||
|
// DISABLED until this translation is more thoroughly reviewed -prf
|
||||||
|
// import {messages as messagesEs} from '#/locale/locales/es/messages'
|
||||||
|
|
||||||
import {sanitizeAppLanguageSetting} from '#/locale/helpers'
|
import {sanitizeAppLanguageSetting} from '#/locale/helpers'
|
||||||
import {AppLanguage} from '#/locale/languages'
|
import {AppLanguage} from '#/locale/languages'
|
||||||
|
|
||||||
@@ -21,6 +27,20 @@ export async function dynamicActivate(locale: AppLanguage) {
|
|||||||
i18n.loadAndActivate({locale, messages: messagesJa})
|
i18n.loadAndActivate({locale, messages: messagesJa})
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
case AppLanguage.fr: {
|
||||||
|
i18n.loadAndActivate({locale, messages: messagesFr})
|
||||||
|
break
|
||||||
|
}
|
||||||
|
// DISABLED until this translation is fixed -prf
|
||||||
|
// case AppLanguage.de: {
|
||||||
|
// i18n.loadAndActivate({locale, messages: messagesDe})
|
||||||
|
// break
|
||||||
|
// }
|
||||||
|
// DISABLED until this translation is more thoroughly reviewed -prf
|
||||||
|
// case AppLanguage.es: {
|
||||||
|
// i18n.loadAndActivate({locale, messages: messagesEs})
|
||||||
|
// break
|
||||||
|
// }
|
||||||
default: {
|
default: {
|
||||||
i18n.loadAndActivate({locale, messages: messagesEn})
|
i18n.loadAndActivate({locale, messages: messagesEn})
|
||||||
break
|
break
|
||||||
|
|||||||
@@ -20,6 +20,20 @@ export async function dynamicActivate(locale: AppLanguage) {
|
|||||||
mod = await import(`./locales/ja/messages`)
|
mod = await import(`./locales/ja/messages`)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
case AppLanguage.fr: {
|
||||||
|
mod = await import(`./locales/fr/messages`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
// DISABLED until this translation is fixed -prf
|
||||||
|
// case AppLanguage.de: {
|
||||||
|
// mod = await import(`./locales/de/messages`)
|
||||||
|
// break
|
||||||
|
// }
|
||||||
|
// DISABLED until this translation is more thoroughly reviewed -prf
|
||||||
|
// case AppLanguage.es: {
|
||||||
|
// mod = await import(`./locales/es/messages`)
|
||||||
|
// break
|
||||||
|
// }
|
||||||
default: {
|
default: {
|
||||||
mod = await import(`./locales/en/messages`)
|
mod = await import(`./locales/en/messages`)
|
||||||
break
|
break
|
||||||
|
|||||||
@@ -8,6 +8,11 @@ export enum AppLanguage {
|
|||||||
en = 'en',
|
en = 'en',
|
||||||
hi = 'hi',
|
hi = 'hi',
|
||||||
ja = 'ja',
|
ja = 'ja',
|
||||||
|
fr = 'fr',
|
||||||
|
// DISABLED until this translation is fixed -prf
|
||||||
|
// de = 'de',
|
||||||
|
// DISABLED until this translation is more thoroughly reviewed -prf
|
||||||
|
// es = 'es',
|
||||||
}
|
}
|
||||||
|
|
||||||
interface AppLanguageConfig {
|
interface AppLanguageConfig {
|
||||||
@@ -19,6 +24,11 @@ export const APP_LANGUAGES: AppLanguageConfig[] = [
|
|||||||
{code2: AppLanguage.en, name: 'English'},
|
{code2: AppLanguage.en, name: 'English'},
|
||||||
{code2: AppLanguage.hi, name: 'हिंदी'},
|
{code2: AppLanguage.hi, name: 'हिंदी'},
|
||||||
{code2: AppLanguage.ja, name: '日本語'},
|
{code2: AppLanguage.ja, name: '日本語'},
|
||||||
|
{code2: AppLanguage.fr, name: 'Français'},
|
||||||
|
// DISABLED until this translation is fixed -prf
|
||||||
|
// {code2: AppLanguage.de, name: 'Deutsch'},
|
||||||
|
// DISABLED until this translation is more thoroughly reviewed -prf
|
||||||
|
// {code2: AppLanguage.es, name: 'Español'},
|
||||||
]
|
]
|
||||||
|
|
||||||
export const LANGUAGES: Language[] = [
|
export const LANGUAGES: Language[] = [
|
||||||
|
|||||||
@@ -287,15 +287,11 @@ export class Logger {
|
|||||||
metadata: Metadata = {},
|
metadata: Metadata = {},
|
||||||
) {
|
) {
|
||||||
if (!this.enabled) return
|
if (!this.enabled) return
|
||||||
if (!enabledLogLevels[this.level].includes(level)) return
|
|
||||||
|
|
||||||
const timestamp = Date.now()
|
const timestamp = Date.now()
|
||||||
const meta = metadata || {}
|
const meta = metadata || {}
|
||||||
|
|
||||||
for (const transport of this.transports) {
|
// send every log to syslog
|
||||||
transport(level, message, meta, timestamp)
|
|
||||||
}
|
|
||||||
|
|
||||||
add({
|
add({
|
||||||
id: nanoid(),
|
id: nanoid(),
|
||||||
timestamp,
|
timestamp,
|
||||||
@@ -303,6 +299,12 @@ export class Logger {
|
|||||||
message,
|
message,
|
||||||
metadata: meta,
|
metadata: meta,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if (!enabledLogLevels[this.level].includes(level)) return
|
||||||
|
|
||||||
|
for (const transport of this.transports) {
|
||||||
|
transport(level, message, meta, timestamp)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ let entries: ConsoleTransportEntry[] = []
|
|||||||
|
|
||||||
export function add(entry: ConsoleTransportEntry) {
|
export function add(entry: ConsoleTransportEntry) {
|
||||||
entries.unshift(entry)
|
entries.unshift(entry)
|
||||||
entries = entries.slice(0, 50)
|
entries = entries.slice(0, 500)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getEntries() {
|
export function getEntries() {
|
||||||
|
|||||||
@@ -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[]
|
||||||
|
|||||||
@@ -108,6 +108,7 @@ 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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -37,6 +37,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>
|
||||||
|
|
||||||
@@ -66,4 +67,5 @@ export const defaults: Schema = {
|
|||||||
onboarding: {
|
onboarding: {
|
||||||
step: 'Home',
|
step: 'Home',
|
||||||
},
|
},
|
||||||
|
hiddenPosts: [],
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,21 @@
|
|||||||
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'
|
||||||
|
|
||||||
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 * from './hidden-posts'
|
||||||
|
|
||||||
export function Provider({children}: React.PropsWithChildren<{}>) {
|
export function Provider({children}: React.PropsWithChildren<{}>) {
|
||||||
return (
|
return (
|
||||||
<LanguagesProvider>
|
<LanguagesProvider>
|
||||||
<AltTextRequiredProvider>{children}</AltTextRequiredProvider>
|
<AltTextRequiredProvider>
|
||||||
|
<HiddenPostsProvider>{children}</HiddenPostsProvider>
|
||||||
|
</AltTextRequiredProvider>
|
||||||
</LanguagesProvider>
|
</LanguagesProvider>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
getModerationOpts,
|
getModerationOpts,
|
||||||
useModerationOpts,
|
useModerationOpts,
|
||||||
} from './preferences'
|
} from './preferences'
|
||||||
|
import {isInvalidHandle} from '#/lib/strings/handles'
|
||||||
|
|
||||||
const DEFAULT_MOD_OPTS = getModerationOpts({
|
const DEFAULT_MOD_OPTS = getModerationOpts({
|
||||||
userDid: '',
|
userDid: '',
|
||||||
@@ -111,7 +112,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'
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -119,7 +120,7 @@ function prefixMatch(
|
|||||||
prefix: string,
|
prefix: string,
|
||||||
info: AppBskyActorDefs.ProfileViewBasic,
|
info: AppBskyActorDefs.ProfileViewBasic,
|
||||||
): boolean {
|
): boolean {
|
||||||
if (info.handle.includes(prefix)) {
|
if (!isInvalidHandle(info.handle) && info.handle.includes(prefix)) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
if (info.displayName?.toLocaleLowerCase().includes(prefix)) {
|
if (info.displayName?.toLocaleLowerCase().includes(prefix)) {
|
||||||
|
|||||||
@@ -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>(() => {
|
||||||
@@ -249,6 +251,7 @@ export function usePinnedFeedsInfos(): {
|
|||||||
// these requests can fail, need to filter those out
|
// these requests can fail, need to filter those out
|
||||||
try {
|
try {
|
||||||
return await queryClient.fetchQuery({
|
return await queryClient.fetchQuery({
|
||||||
|
staleTime: STALE.SECONDS.FIFTEEN,
|
||||||
queryKey: feedSourceInfoQueryKey({uri}),
|
queryKey: feedSourceInfoQueryKey({uri}),
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
const type = getFeedTypeFromUri(uri)
|
const type = getFeedTypeFromUri(uri)
|
||||||
@@ -283,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}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -89,6 +89,9 @@ 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}: {invalidate?: boolean} = {}) {
|
||||||
|
|||||||
@@ -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,5 @@
|
|||||||
import React, {useCallback, useEffect, useRef} from 'react'
|
import React, {useCallback, useEffect, useRef} from 'react'
|
||||||
import {
|
import {AppBskyFeedDefs, AppBskyFeedPost, PostModeration} from '@atproto/api'
|
||||||
AppBskyFeedDefs,
|
|
||||||
AppBskyFeedPost,
|
|
||||||
moderatePost,
|
|
||||||
PostModeration,
|
|
||||||
} from '@atproto/api'
|
|
||||||
import {
|
import {
|
||||||
useInfiniteQuery,
|
useInfiniteQuery,
|
||||||
InfiniteData,
|
InfiniteData,
|
||||||
@@ -12,6 +7,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'
|
||||||
|
|||||||
@@ -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'
|
||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -189,6 +189,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
|
|||||||
},
|
},
|
||||||
logger.DebugContext.session,
|
logger.DebugContext.session,
|
||||||
)
|
)
|
||||||
|
track('Try Create Account')
|
||||||
|
|
||||||
const agent = new BskyAgent({service})
|
const agent = new BskyAgent({service})
|
||||||
|
|
||||||
@@ -231,6 +232,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
|
|||||||
},
|
},
|
||||||
logger.DebugContext.session,
|
logger.DebugContext.session,
|
||||||
)
|
)
|
||||||
|
track('Create Account')
|
||||||
},
|
},
|
||||||
[upsertAccount, queryClient],
|
[upsertAccount, queryClient],
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -3,8 +3,9 @@ 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 {msg} from '@lingui/macro'
|
||||||
|
import {useNavigation} from '@react-navigation/native'
|
||||||
|
|
||||||
import {isIOS} from 'platform/detection'
|
import {isIOS, isNative} from 'platform/detection'
|
||||||
import {Login} from 'view/com/auth/login/Login'
|
import {Login} from 'view/com/auth/login/Login'
|
||||||
import {CreateAccount} from 'view/com/auth/create/CreateAccount'
|
import {CreateAccount} from 'view/com/auth/create/CreateAccount'
|
||||||
import {ErrorBoundary} from 'view/com/util/ErrorBoundary'
|
import {ErrorBoundary} from 'view/com/util/ErrorBoundary'
|
||||||
@@ -18,6 +19,9 @@ import {
|
|||||||
useLoggedOutView,
|
useLoggedOutView,
|
||||||
useLoggedOutViewControls,
|
useLoggedOutViewControls,
|
||||||
} from '#/state/shell/logged-out'
|
} from '#/state/shell/logged-out'
|
||||||
|
import {useSession} from '#/state/session'
|
||||||
|
import {Text} from '#/view/com/util/text/Text'
|
||||||
|
import {NavigationProp} from 'lib/routes/types'
|
||||||
|
|
||||||
enum ScreenState {
|
enum ScreenState {
|
||||||
S_LoginOrCreateAccount,
|
S_LoginOrCreateAccount,
|
||||||
@@ -26,6 +30,7 @@ enum ScreenState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function LoggedOut({onDismiss}: {onDismiss?: () => void}) {
|
export function LoggedOut({onDismiss}: {onDismiss?: () => void}) {
|
||||||
|
const {hasSession} = useSession()
|
||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
const pal = usePalette('default')
|
const pal = usePalette('default')
|
||||||
const setMinimalShellMode = useSetMinimalShellMode()
|
const setMinimalShellMode = useSetMinimalShellMode()
|
||||||
@@ -40,6 +45,8 @@ export function LoggedOut({onDismiss}: {onDismiss?: () => void}) {
|
|||||||
)
|
)
|
||||||
const {isMobile} = useWebMediaQueries()
|
const {isMobile} = useWebMediaQueries()
|
||||||
const {clearRequestedAccount} = useLoggedOutViewControls()
|
const {clearRequestedAccount} = useLoggedOutViewControls()
|
||||||
|
const navigation = useNavigation<NavigationProp>()
|
||||||
|
const isFirstScreen = screenState === ScreenState.S_LoginOrCreateAccount
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
screen('Login')
|
screen('Login')
|
||||||
@@ -53,6 +60,10 @@ export function LoggedOut({onDismiss}: {onDismiss?: () => void}) {
|
|||||||
clearRequestedAccount()
|
clearRequestedAccount()
|
||||||
}, [clearRequestedAccount, onDismiss])
|
}, [clearRequestedAccount, onDismiss])
|
||||||
|
|
||||||
|
const onPressSearch = React.useCallback(() => {
|
||||||
|
navigation.navigate(`SearchTab`)
|
||||||
|
}, [navigation])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View
|
<View
|
||||||
testID="noSessionView"
|
testID="noSessionView"
|
||||||
@@ -65,7 +76,7 @@ export function LoggedOut({onDismiss}: {onDismiss?: () => void}) {
|
|||||||
},
|
},
|
||||||
]}>
|
]}>
|
||||||
<ErrorBoundary>
|
<ErrorBoundary>
|
||||||
{onDismiss && (
|
{onDismiss ? (
|
||||||
<Pressable
|
<Pressable
|
||||||
accessibilityHint={_(msg`Go back`)}
|
accessibilityHint={_(msg`Go back`)}
|
||||||
accessibilityLabel={_(msg`Go back`)}
|
accessibilityLabel={_(msg`Go back`)}
|
||||||
@@ -88,7 +99,37 @@ export function LoggedOut({onDismiss}: {onDismiss?: () => void}) {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Pressable>
|
</Pressable>
|
||||||
)}
|
) : isNative && !hasSession && isFirstScreen ? (
|
||||||
|
<Pressable
|
||||||
|
accessibilityHint={_(msg`Search for users`)}
|
||||||
|
accessibilityLabel={_(msg`Search for users`)}
|
||||||
|
accessibilityRole="button"
|
||||||
|
style={{
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: 4,
|
||||||
|
position: 'absolute',
|
||||||
|
top: 20,
|
||||||
|
right: 20,
|
||||||
|
paddingHorizontal: 16,
|
||||||
|
paddingVertical: 8,
|
||||||
|
zIndex: 100,
|
||||||
|
backgroundColor: pal.btn.backgroundColor,
|
||||||
|
borderRadius: 100,
|
||||||
|
}}
|
||||||
|
onPress={onPressSearch}>
|
||||||
|
<Text type="lg-bold" style={[pal.text]}>
|
||||||
|
Search{' '}
|
||||||
|
</Text>
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon="search"
|
||||||
|
size={16}
|
||||||
|
style={{
|
||||||
|
color: String(pal.text.color),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Pressable>
|
||||||
|
) : null}
|
||||||
|
|
||||||
{screenState === ScreenState.S_LoginOrCreateAccount ? (
|
{screenState === ScreenState.S_LoginOrCreateAccount ? (
|
||||||
<SplashScreen
|
<SplashScreen
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import {
|
import {
|
||||||
ActivityIndicator,
|
ActivityIndicator,
|
||||||
KeyboardAvoidingView,
|
|
||||||
ScrollView,
|
ScrollView,
|
||||||
StyleSheet,
|
StyleSheet,
|
||||||
TouchableOpacity,
|
TouchableOpacity,
|
||||||
@@ -28,9 +27,10 @@ import {IS_PROD} from '#/lib/constants'
|
|||||||
import {Step1} from './Step1'
|
import {Step1} from './Step1'
|
||||||
import {Step2} from './Step2'
|
import {Step2} from './Step2'
|
||||||
import {Step3} from './Step3'
|
import {Step3} from './Step3'
|
||||||
|
import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
|
||||||
|
|
||||||
export function CreateAccount({onPressBack}: {onPressBack: () => void}) {
|
export function CreateAccount({onPressBack}: {onPressBack: () => void}) {
|
||||||
const {track, screen} = useAnalytics()
|
const {screen} = useAnalytics()
|
||||||
const pal = usePalette('default')
|
const pal = usePalette('default')
|
||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
const [uiState, uiDispatch] = useCreateAccount()
|
const [uiState, uiDispatch] = useCreateAccount()
|
||||||
@@ -38,6 +38,7 @@ export function CreateAccount({onPressBack}: {onPressBack: () => void}) {
|
|||||||
const {createAccount} = useSessionApi()
|
const {createAccount} = useSessionApi()
|
||||||
const {mutate: setBirthDate} = usePreferencesSetBirthDateMutation()
|
const {mutate: setBirthDate} = usePreferencesSetBirthDateMutation()
|
||||||
const {mutate: setSavedFeeds} = useSetSaveFeedsMutation()
|
const {mutate: setSavedFeeds} = useSetSaveFeedsMutation()
|
||||||
|
const {isTabletOrDesktop} = useWebMediaQueries()
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
screen('CreateAccount')
|
screen('CreateAccount')
|
||||||
@@ -93,21 +94,17 @@ export function CreateAccount({onPressBack}: {onPressBack: () => void}) {
|
|||||||
uiDispatch,
|
uiDispatch,
|
||||||
_,
|
_,
|
||||||
})
|
})
|
||||||
track('Create Account')
|
|
||||||
setBirthDate({birthDate: uiState.birthDate})
|
setBirthDate({birthDate: uiState.birthDate})
|
||||||
if (IS_PROD(uiState.serviceUrl)) {
|
if (IS_PROD(uiState.serviceUrl)) {
|
||||||
setSavedFeeds(DEFAULT_PROD_FEEDS)
|
setSavedFeeds(DEFAULT_PROD_FEEDS)
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
// dont need to handle here
|
// dont need to handle here
|
||||||
} finally {
|
|
||||||
track('Try Create Account')
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [
|
}, [
|
||||||
uiState,
|
uiState,
|
||||||
uiDispatch,
|
uiDispatch,
|
||||||
track,
|
|
||||||
onboardingDispatch,
|
onboardingDispatch,
|
||||||
createAccount,
|
createAccount,
|
||||||
setBirthDate,
|
setBirthDate,
|
||||||
@@ -124,7 +121,6 @@ export function CreateAccount({onPressBack}: {onPressBack: () => void}) {
|
|||||||
title={_(msg`Create Account`)}
|
title={_(msg`Create Account`)}
|
||||||
description={_(msg`We're so excited to have you join us!`)}>
|
description={_(msg`We're so excited to have you join us!`)}>
|
||||||
<ScrollView testID="createAccount" style={pal.view}>
|
<ScrollView testID="createAccount" style={pal.view}>
|
||||||
<KeyboardAvoidingView behavior="padding">
|
|
||||||
<View style={styles.stepContainer}>
|
<View style={styles.stepContainer}>
|
||||||
{uiState.step === 1 && (
|
{uiState.step === 1 && (
|
||||||
<Step1 uiState={uiState} uiDispatch={uiDispatch} />
|
<Step1 uiState={uiState} uiDispatch={uiDispatch} />
|
||||||
@@ -180,8 +176,7 @@ export function CreateAccount({onPressBack}: {onPressBack: () => void}) {
|
|||||||
</>
|
</>
|
||||||
) : undefined}
|
) : undefined}
|
||||||
</View>
|
</View>
|
||||||
<View style={s.footerSpacer} />
|
<View style={{height: isTabletOrDesktop ? 50 : 400}} />
|
||||||
</KeyboardAvoidingView>
|
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
</LoggedOutLayout>
|
</LoggedOutLayout>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -13,6 +13,17 @@ import {isWeb} from 'platform/detection'
|
|||||||
import {Trans, msg} from '@lingui/macro'
|
import {Trans, msg} from '@lingui/macro'
|
||||||
import {useLingui} from '@lingui/react'
|
import {useLingui} from '@lingui/react'
|
||||||
import {useModalControls} from '#/state/modals'
|
import {useModalControls} from '#/state/modals'
|
||||||
|
import {logger} from '#/logger'
|
||||||
|
|
||||||
|
function sanitizeDate(date: Date): Date {
|
||||||
|
if (!date || date.toString() === 'Invalid Date') {
|
||||||
|
logger.error(`Create account: handled invalid date for birthDate`, {
|
||||||
|
hasDate: !!date,
|
||||||
|
})
|
||||||
|
return new Date()
|
||||||
|
}
|
||||||
|
return date
|
||||||
|
}
|
||||||
|
|
||||||
/** STEP 2: Your account
|
/** STEP 2: Your account
|
||||||
* @field Invite code or waitlist
|
* @field Invite code or waitlist
|
||||||
@@ -38,6 +49,10 @@ export function Step2({
|
|||||||
openModal({name: 'waitlist'})
|
openModal({name: 'waitlist'})
|
||||||
}, [openModal])
|
}, [openModal])
|
||||||
|
|
||||||
|
const birthDate = React.useMemo(() => {
|
||||||
|
return sanitizeDate(uiState.birthDate)
|
||||||
|
}, [uiState.birthDate])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View>
|
<View>
|
||||||
<StepHeader step="2" title={_(msg`Your account`)} />
|
<StepHeader step="2" title={_(msg`Your account`)} />
|
||||||
@@ -56,6 +71,9 @@ export function Step2({
|
|||||||
onChange={value => uiDispatch({type: 'set-invite-code', value})}
|
onChange={value => uiDispatch({type: 'set-invite-code', value})}
|
||||||
accessibilityLabel={_(msg`Invite code`)}
|
accessibilityLabel={_(msg`Invite code`)}
|
||||||
accessibilityHint="Input invite code to proceed"
|
accessibilityHint="Input invite code to proceed"
|
||||||
|
autoCapitalize="none"
|
||||||
|
autoComplete="off"
|
||||||
|
autoCorrect={false}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
@@ -90,6 +108,9 @@ export function Step2({
|
|||||||
accessibilityLabel={_(msg`Email`)}
|
accessibilityLabel={_(msg`Email`)}
|
||||||
accessibilityHint="Input email for Bluesky waitlist"
|
accessibilityHint="Input email for Bluesky waitlist"
|
||||||
accessibilityLabelledBy="email"
|
accessibilityLabelledBy="email"
|
||||||
|
autoCapitalize="none"
|
||||||
|
autoComplete="off"
|
||||||
|
autoCorrect={false}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
@@ -111,6 +132,9 @@ export function Step2({
|
|||||||
accessibilityLabel={_(msg`Password`)}
|
accessibilityLabel={_(msg`Password`)}
|
||||||
accessibilityHint="Set password"
|
accessibilityHint="Set password"
|
||||||
accessibilityLabelledBy="password"
|
accessibilityLabelledBy="password"
|
||||||
|
autoCapitalize="none"
|
||||||
|
autoComplete="off"
|
||||||
|
autoCorrect={false}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
@@ -122,8 +146,9 @@ export function Step2({
|
|||||||
<Trans>Your birth date</Trans>
|
<Trans>Your birth date</Trans>
|
||||||
</Text>
|
</Text>
|
||||||
<DateInput
|
<DateInput
|
||||||
|
handleAsUTC
|
||||||
testID="birthdayInput"
|
testID="birthdayInput"
|
||||||
value={uiState.birthDate}
|
value={birthDate}
|
||||||
onChange={value => uiDispatch({type: 'set-birth-date', value})}
|
onChange={value => uiDispatch({type: 'set-birth-date', value})}
|
||||||
buttonType="default-light"
|
buttonType="default-light"
|
||||||
buttonStyle={[pal.border, styles.dateInputButton]}
|
buttonStyle={[pal.border, styles.dateInputButton]}
|
||||||
|
|||||||
@@ -144,7 +144,7 @@ export async function submit({
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function is13(state: CreateAccountState) {
|
export function is13(state: CreateAccountState) {
|
||||||
return getAge(state.birthDate) >= 18
|
return getAge(state.birthDate) >= 13
|
||||||
}
|
}
|
||||||
|
|
||||||
export function is18(state: CreateAccountState) {
|
export function is18(state: CreateAccountState) {
|
||||||
|
|||||||
@@ -174,6 +174,7 @@ export const LoginForm = ({
|
|||||||
autoCorrect={false}
|
autoCorrect={false}
|
||||||
autoComplete="username"
|
autoComplete="username"
|
||||||
returnKeyType="next"
|
returnKeyType="next"
|
||||||
|
textContentType="username"
|
||||||
onSubmitEditing={() => {
|
onSubmitEditing={() => {
|
||||||
passwordInputRef.current?.focus()
|
passwordInputRef.current?.focus()
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -207,7 +207,11 @@ export const ComposePost = observer(function ComposePost({
|
|||||||
setError('')
|
setError('')
|
||||||
|
|
||||||
if (richtext.text.trim().length === 0 && gallery.isEmpty && !extLink) {
|
if (richtext.text.trim().length === 0 && gallery.isEmpty && !extLink) {
|
||||||
setError('Did you want to say anything?')
|
setError(_(msg`Did you want to say anything?`))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (extLink?.isLoading) {
|
||||||
|
setError(_(msg`Please wait for your link card to finish loading`))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -438,7 +442,7 @@ export const ComposePost = observer(function ComposePost({
|
|||||||
accessibilityLabel={_(msg`Add link card`)}
|
accessibilityLabel={_(msg`Add link card`)}
|
||||||
accessibilityHint={`Creates a card with a thumbnail. The card links to ${url}`}>
|
accessibilityHint={`Creates a card with a thumbnail. The card links to ${url}`}>
|
||||||
<Text style={pal.text}>
|
<Text style={pal.text}>
|
||||||
<Trans>Add link card:</Trans>
|
<Trans>Add link card:</Trans>{' '}
|
||||||
<Text style={[pal.link, s.ml5]}>{toShortUrl(url)}</Text>
|
<Text style={[pal.link, s.ml5]}>{toShortUrl(url)}</Text>
|
||||||
</Text>
|
</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
@@ -452,7 +456,7 @@ export const ComposePost = observer(function ComposePost({
|
|||||||
<OpenCameraBtn gallery={gallery} />
|
<OpenCameraBtn gallery={gallery} />
|
||||||
</>
|
</>
|
||||||
) : null}
|
) : null}
|
||||||
{isDesktop ? <EmojiPickerButton /> : null}
|
{!isMobile ? <EmojiPickerButton /> : null}
|
||||||
<View style={s.flex1} />
|
<View style={s.flex1} />
|
||||||
<SelectLangBtn />
|
<SelectLangBtn />
|
||||||
<CharProgress count={graphemeLength} />
|
<CharProgress count={graphemeLength} />
|
||||||
|
|||||||
@@ -215,7 +215,13 @@ export const TextInput = forwardRef(function TextInputImpl(
|
|||||||
autoFocus={true}
|
autoFocus={true}
|
||||||
allowFontScaling
|
allowFontScaling
|
||||||
multiline
|
multiline
|
||||||
style={[pal.text, styles.textInput, styles.textInputFormatting]}
|
numberOfLines={4}
|
||||||
|
style={[
|
||||||
|
pal.text,
|
||||||
|
styles.textInput,
|
||||||
|
styles.textInputFormatting,
|
||||||
|
{textAlignVertical: 'top'},
|
||||||
|
]}
|
||||||
{...props}>
|
{...props}>
|
||||||
{textDecorated}
|
{textDecorated}
|
||||||
</PasteInput>
|
</PasteInput>
|
||||||
|
|||||||
@@ -134,7 +134,7 @@ const MentionList = forwardRef<MentionListRef, SuggestionProps>(
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
if (event.key === 'Enter') {
|
if (event.key === 'Enter' || event.key === 'Tab') {
|
||||||
enterHandler()
|
enterHandler()
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -76,7 +76,7 @@ export function EmojiPicker({close}: {close: () => void}) {
|
|||||||
return (await import('./EmojiPickerData.json')).default
|
return (await import('./EmojiPickerData.json')).default
|
||||||
}}
|
}}
|
||||||
onEmojiSelect={onInsert}
|
onEmojiSelect={onInsert}
|
||||||
autoFocus={false}
|
autoFocus={true}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
</TouchableWithoutFeedback>
|
</TouchableWithoutFeedback>
|
||||||
@@ -96,6 +96,7 @@ const styles = StyleSheet.create({
|
|||||||
},
|
},
|
||||||
trigger: {
|
trigger: {
|
||||||
backgroundColor: 'transparent',
|
backgroundColor: 'transparent',
|
||||||
|
// @ts-ignore web only -prf
|
||||||
border: 'none',
|
border: 'none',
|
||||||
paddingTop: 4,
|
paddingTop: 4,
|
||||||
paddingLeft: 12,
|
paddingLeft: 12,
|
||||||
|
|||||||
@@ -1,12 +1,5 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import {
|
import {Dimensions, StyleProp, StyleSheet, View, ViewStyle} from 'react-native'
|
||||||
Dimensions,
|
|
||||||
RefreshControl,
|
|
||||||
StyleProp,
|
|
||||||
StyleSheet,
|
|
||||||
View,
|
|
||||||
ViewStyle,
|
|
||||||
} from 'react-native'
|
|
||||||
import {useQueryClient} from '@tanstack/react-query'
|
import {useQueryClient} from '@tanstack/react-query'
|
||||||
import {List, ListRef} from '../util/List'
|
import {List, ListRef} from '../util/List'
|
||||||
import {FeedSourceCardLoaded} from './FeedSourceCard'
|
import {FeedSourceCardLoaded} from './FeedSourceCard'
|
||||||
@@ -180,22 +173,14 @@ export const ProfileFeedgens = React.forwardRef<
|
|||||||
data={items}
|
data={items}
|
||||||
keyExtractor={(item: any) => item._reactKey || item.uri}
|
keyExtractor={(item: any) => item._reactKey || item.uri}
|
||||||
renderItem={renderItemInner}
|
renderItem={renderItemInner}
|
||||||
refreshControl={
|
|
||||||
<RefreshControl
|
|
||||||
refreshing={isPTRing}
|
refreshing={isPTRing}
|
||||||
onRefresh={onRefresh}
|
onRefresh={onRefresh}
|
||||||
tintColor={pal.colors.text}
|
headerOffset={headerOffset}
|
||||||
titleColor={pal.colors.text}
|
|
||||||
progressViewOffset={headerOffset}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
contentContainerStyle={{
|
contentContainerStyle={{
|
||||||
minHeight: Dimensions.get('window').height * 1.5,
|
minHeight: Dimensions.get('window').height * 1.5,
|
||||||
}}
|
}}
|
||||||
style={{paddingTop: headerOffset}}
|
|
||||||
indicatorStyle={theme.colorScheme === 'dark' ? 'white' : 'black'}
|
indicatorStyle={theme.colorScheme === 'dark' ? 'white' : 'black'}
|
||||||
removeClippedSubviews={true}
|
removeClippedSubviews={true}
|
||||||
contentOffset={{x: 0, y: headerOffset * -1}}
|
|
||||||
// @ts-ignore our .web version only -prf
|
// @ts-ignore our .web version only -prf
|
||||||
desktopFixedHeight
|
desktopFixedHeight
|
||||||
onEndReached={onEndReached}
|
onEndReached={onEndReached}
|
||||||
|
|||||||
@@ -320,6 +320,7 @@ const ImageItem = ({
|
|||||||
accessibilityLabel={imageSrc.alt}
|
accessibilityLabel={imageSrc.alt}
|
||||||
accessibilityHint=""
|
accessibilityHint=""
|
||||||
onLoad={() => setIsLoaded(true)}
|
onLoad={() => setIsLoaded(true)}
|
||||||
|
cachePolicy="memory"
|
||||||
/>
|
/>
|
||||||
</GestureDetector>
|
</GestureDetector>
|
||||||
</Animated.View>
|
</Animated.View>
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import React from 'react'
|
|||||||
import {
|
import {
|
||||||
ActivityIndicator,
|
ActivityIndicator,
|
||||||
Dimensions,
|
Dimensions,
|
||||||
RefreshControl,
|
|
||||||
StyleProp,
|
StyleProp,
|
||||||
View,
|
View,
|
||||||
ViewStyle,
|
ViewStyle,
|
||||||
@@ -15,7 +14,6 @@ import {LoadMoreRetryBtn} from '../util/LoadMoreRetryBtn'
|
|||||||
import {ProfileCard} from '../profile/ProfileCard'
|
import {ProfileCard} from '../profile/ProfileCard'
|
||||||
import {Button} from '../util/forms/Button'
|
import {Button} from '../util/forms/Button'
|
||||||
import {useAnalytics} from 'lib/analytics/analytics'
|
import {useAnalytics} from 'lib/analytics/analytics'
|
||||||
import {usePalette} from 'lib/hooks/usePalette'
|
|
||||||
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
|
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
|
||||||
import {useListMembersQuery} from '#/state/queries/list-members'
|
import {useListMembersQuery} from '#/state/queries/list-members'
|
||||||
import {logger} from '#/logger'
|
import {logger} from '#/logger'
|
||||||
@@ -51,7 +49,6 @@ export function ListMembers({
|
|||||||
headerOffset?: number
|
headerOffset?: number
|
||||||
desktopFixedHeightOffset?: number
|
desktopFixedHeightOffset?: number
|
||||||
}) {
|
}) {
|
||||||
const pal = usePalette('default')
|
|
||||||
const {track} = useAnalytics()
|
const {track} = useAnalytics()
|
||||||
const [isRefreshing, setIsRefreshing] = React.useState(false)
|
const [isRefreshing, setIsRefreshing] = React.useState(false)
|
||||||
const {isMobile} = useWebMediaQueries()
|
const {isMobile} = useWebMediaQueries()
|
||||||
@@ -183,6 +180,7 @@ export function ListMembers({
|
|||||||
profile={(item as AppBskyGraphDefs.ListItemView).subject}
|
profile={(item as AppBskyGraphDefs.ListItemView).subject}
|
||||||
renderButton={renderMemberButton}
|
renderButton={renderMemberButton}
|
||||||
style={{paddingHorizontal: isMobile ? 8 : 14, paddingVertical: 4}}
|
style={{paddingHorizontal: isMobile ? 8 : 14, paddingVertical: 4}}
|
||||||
|
noModFilter
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
@@ -215,24 +213,16 @@ export function ListMembers({
|
|||||||
renderItem={renderItem}
|
renderItem={renderItem}
|
||||||
ListHeaderComponent={renderHeader}
|
ListHeaderComponent={renderHeader}
|
||||||
ListFooterComponent={Footer}
|
ListFooterComponent={Footer}
|
||||||
refreshControl={
|
|
||||||
<RefreshControl
|
|
||||||
refreshing={isRefreshing}
|
refreshing={isRefreshing}
|
||||||
onRefresh={onRefresh}
|
onRefresh={onRefresh}
|
||||||
tintColor={pal.colors.text}
|
headerOffset={headerOffset}
|
||||||
titleColor={pal.colors.text}
|
|
||||||
progressViewOffset={headerOffset}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
contentContainerStyle={{
|
contentContainerStyle={{
|
||||||
minHeight: Dimensions.get('window').height * 1.5,
|
minHeight: Dimensions.get('window').height * 1.5,
|
||||||
}}
|
}}
|
||||||
style={{paddingTop: headerOffset}}
|
|
||||||
onScrolledDownChange={onScrolledDownChange}
|
onScrolledDownChange={onScrolledDownChange}
|
||||||
onEndReached={onEndReached}
|
onEndReached={onEndReached}
|
||||||
onEndReachedThreshold={0.6}
|
onEndReachedThreshold={0.6}
|
||||||
removeClippedSubviews={true}
|
removeClippedSubviews={true}
|
||||||
contentOffset={{x: 0, y: headerOffset * -1}}
|
|
||||||
// @ts-ignore our .web version only -prf
|
// @ts-ignore our .web version only -prf
|
||||||
desktopFixedHeight={desktopFixedHeightOffset || true}
|
desktopFixedHeight={desktopFixedHeightOffset || true}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -119,11 +119,11 @@ export function MyLists({
|
|||||||
[error, onRefresh, renderItem, pal],
|
[error, onRefresh, renderItem, pal],
|
||||||
)
|
)
|
||||||
|
|
||||||
const FlatListCom = inline ? RNFlatList : List
|
if (inline) {
|
||||||
return (
|
return (
|
||||||
<View testID={testID} style={style}>
|
<View testID={testID} style={style}>
|
||||||
{items.length > 0 && (
|
{items.length > 0 && (
|
||||||
<FlatListCom
|
<RNFlatList
|
||||||
testID={testID ? `${testID}-flatlist` : undefined}
|
testID={testID ? `${testID}-flatlist` : undefined}
|
||||||
data={items}
|
data={items}
|
||||||
keyExtractor={item => (item.uri ? item.uri : item._reactKey)}
|
keyExtractor={item => (item.uri ? item.uri : item._reactKey)}
|
||||||
@@ -144,6 +144,26 @@ export function MyLists({
|
|||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
|
} else {
|
||||||
|
return (
|
||||||
|
<View testID={testID} style={style}>
|
||||||
|
{items.length > 0 && (
|
||||||
|
<List
|
||||||
|
testID={testID ? `${testID}-flatlist` : undefined}
|
||||||
|
data={items}
|
||||||
|
keyExtractor={item => (item.uri ? item.uri : item._reactKey)}
|
||||||
|
renderItem={renderItemInner}
|
||||||
|
refreshing={isPTRing}
|
||||||
|
onRefresh={onRefresh}
|
||||||
|
contentContainerStyle={[s.contentContainer]}
|
||||||
|
removeClippedSubviews={true}
|
||||||
|
// @ts-ignore our .web version only -prf
|
||||||
|
desktopFixedHeight
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
|
|||||||
@@ -1,12 +1,5 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import {
|
import {Dimensions, StyleProp, StyleSheet, View, ViewStyle} from 'react-native'
|
||||||
Dimensions,
|
|
||||||
RefreshControl,
|
|
||||||
StyleProp,
|
|
||||||
StyleSheet,
|
|
||||||
View,
|
|
||||||
ViewStyle,
|
|
||||||
} from 'react-native'
|
|
||||||
import {useQueryClient} from '@tanstack/react-query'
|
import {useQueryClient} from '@tanstack/react-query'
|
||||||
import {List, ListRef} from '../util/List'
|
import {List, ListRef} from '../util/List'
|
||||||
import {ListCard} from './ListCard'
|
import {ListCard} from './ListCard'
|
||||||
@@ -182,22 +175,14 @@ export const ProfileLists = React.forwardRef<SectionRef, ProfileListsProps>(
|
|||||||
data={items}
|
data={items}
|
||||||
keyExtractor={(item: any) => item._reactKey}
|
keyExtractor={(item: any) => item._reactKey}
|
||||||
renderItem={renderItemInner}
|
renderItem={renderItemInner}
|
||||||
refreshControl={
|
|
||||||
<RefreshControl
|
|
||||||
refreshing={isPTRing}
|
refreshing={isPTRing}
|
||||||
onRefresh={onRefresh}
|
onRefresh={onRefresh}
|
||||||
tintColor={pal.colors.text}
|
headerOffset={headerOffset}
|
||||||
titleColor={pal.colors.text}
|
|
||||||
progressViewOffset={headerOffset}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
contentContainerStyle={{
|
contentContainerStyle={{
|
||||||
minHeight: Dimensions.get('window').height * 1.5,
|
minHeight: Dimensions.get('window').height * 1.5,
|
||||||
}}
|
}}
|
||||||
style={{paddingTop: headerOffset}}
|
|
||||||
indicatorStyle={theme.colorScheme === 'dark' ? 'white' : 'black'}
|
indicatorStyle={theme.colorScheme === 'dark' ? 'white' : 'black'}
|
||||||
removeClippedSubviews={true}
|
removeClippedSubviews={true}
|
||||||
contentOffset={{x: 0, y: headerOffset * -1}}
|
|
||||||
// @ts-ignore our .web version only -prf
|
// @ts-ignore our .web version only -prf
|
||||||
desktopFixedHeight
|
desktopFixedHeight
|
||||||
onEndReached={onEndReached}
|
onEndReached={onEndReached}
|
||||||
|
|||||||
@@ -80,6 +80,7 @@ export function Component({image}: Props) {
|
|||||||
source={{
|
source={{
|
||||||
uri: image.cropped?.path ?? image.path,
|
uri: image.cropped?.path ?? image.path,
|
||||||
}}
|
}}
|
||||||
|
contentFit="contain"
|
||||||
accessible={true}
|
accessible={true}
|
||||||
accessibilityIgnoresInvertColors
|
accessibilityIgnoresInvertColors
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -62,17 +62,17 @@ export function Component(props: ReportComponentProps) {
|
|||||||
<Text
|
<Text
|
||||||
type="2xl-bold"
|
type="2xl-bold"
|
||||||
style={[pal.text, s.textCenter, {paddingBottom: 8}]}>
|
style={[pal.text, s.textCenter, {paddingBottom: 8}]}>
|
||||||
<Trans>Appeal Decision</Trans>
|
<Trans>Appeal Content Warning</Trans>
|
||||||
</Text>
|
</Text>
|
||||||
<ScrollView>
|
<ScrollView>
|
||||||
<View style={[pal.btn, styles.detailsInputContainer]}>
|
<View style={[pal.btn, styles.detailsInputContainer]}>
|
||||||
<TextInput
|
<TextInput
|
||||||
accessibilityLabel={_(msg`Text input field`)}
|
accessibilityLabel={_(msg`Text input field`)}
|
||||||
accessibilityHint={_(
|
accessibilityHint={_(
|
||||||
msg`Please tell us why you think this decision was incorrect.`,
|
msg`Please tell us why you think this content warning was incorrectly applied!`,
|
||||||
)}
|
)}
|
||||||
placeholder={_(
|
placeholder={_(
|
||||||
msg`Please tell us why you think this decision was incorrect.`,
|
msg`Please tell us why you think this content warning was incorrectly applied!`,
|
||||||
)}
|
)}
|
||||||
placeholderTextColor={pal.textLight.color}
|
placeholderTextColor={pal.textLight.color}
|
||||||
value={details}
|
value={details}
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ import {
|
|||||||
} from '#/state/queries/preferences'
|
} from '#/state/queries/preferences'
|
||||||
import {logger} from '#/logger'
|
import {logger} from '#/logger'
|
||||||
|
|
||||||
export const snapPoints = ['50%']
|
export const snapPoints = ['50%', '90%']
|
||||||
|
|
||||||
function Inner({preferences}: {preferences: UsePreferencesQueryResponse}) {
|
function Inner({preferences}: {preferences: UsePreferencesQueryResponse}) {
|
||||||
const pal = usePalette('default')
|
const pal = usePalette('default')
|
||||||
@@ -63,6 +63,7 @@ function Inner({preferences}: {preferences: UsePreferencesQueryResponse}) {
|
|||||||
|
|
||||||
<View>
|
<View>
|
||||||
<DateInput
|
<DateInput
|
||||||
|
handleAsUTC
|
||||||
testID="birthdayInput"
|
testID="birthdayInput"
|
||||||
value={date}
|
value={date}
|
||||||
onChange={setDate}
|
onChange={setDate}
|
||||||
|
|||||||
@@ -82,7 +82,7 @@ export function ModalsContainer() {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isModalActive) {
|
if (isModalActive) {
|
||||||
bottomSheetRef.current?.expand()
|
bottomSheetRef.current?.snapToIndex(0)
|
||||||
} else {
|
} else {
|
||||||
bottomSheetRef.current?.close()
|
bottomSheetRef.current?.close()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ export function Component({
|
|||||||
|
|
||||||
<ScrollView>
|
<ScrollView>
|
||||||
<Text style={[pal.text, styles.description]}>
|
<Text style={[pal.text, styles.description]}>
|
||||||
Choose "Everybody" or "Nobody"
|
<Trans>Choose "Everybody" or "Nobody"</Trans>
|
||||||
</Text>
|
</Text>
|
||||||
<View style={{flexDirection: 'row', gap: 6, paddingHorizontal: 6}}>
|
<View style={{flexDirection: 'row', gap: 6, paddingHorizontal: 6}}>
|
||||||
<Selectable
|
<Selectable
|
||||||
@@ -86,7 +86,7 @@ export function Component({
|
|||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
<Text style={[pal.text, styles.description]}>
|
<Text style={[pal.text, styles.description]}>
|
||||||
Or combine these options:
|
<Trans>Or combine these options:</Trans>
|
||||||
</Text>
|
</Text>
|
||||||
<View style={{flexDirection: 'column', gap: 4, paddingHorizontal: 6}}>
|
<View style={{flexDirection: 'column', gap: 4, paddingHorizontal: 6}}>
|
||||||
<Selectable
|
<Selectable
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ export function InputIssueDetails({
|
|||||||
accessibilityHint="Add more details to your report">
|
accessibilityHint="Add more details to your report">
|
||||||
<FontAwesomeIcon size={18} icon="angle-left" style={[pal.link]} />
|
<FontAwesomeIcon size={18} icon="angle-left" style={[pal.link]} />
|
||||||
<Text style={[pal.text, s.f18, pal.link]}>
|
<Text style={[pal.text, s.f18, pal.link]}>
|
||||||
|
{' '}
|
||||||
<Trans>Back</Trans>
|
<Trans>Back</Trans>
|
||||||
</Text>
|
</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
|
|||||||
@@ -44,9 +44,9 @@ export function Component(content: ReportComponentProps) {
|
|||||||
const {isMobile} = useWebMediaQueries()
|
const {isMobile} = useWebMediaQueries()
|
||||||
const [isProcessing, setIsProcessing] = useState(false)
|
const [isProcessing, setIsProcessing] = useState(false)
|
||||||
const [showDetailsInput, setShowDetailsInput] = useState(false)
|
const [showDetailsInput, setShowDetailsInput] = useState(false)
|
||||||
const [error, setError] = useState<string>()
|
const [error, setError] = useState<string>('')
|
||||||
const [issue, setIssue] = useState<string>()
|
const [issue, setIssue] = useState<string>('')
|
||||||
const [details, setDetails] = useState<string>()
|
const [details, setDetails] = useState<string>('')
|
||||||
const isAccountReport = 'did' in content
|
const isAccountReport = 'did' in content
|
||||||
const subjectKey = isAccountReport ? content.did : content.uri
|
const subjectKey = isAccountReport ? content.did : content.uri
|
||||||
const atUri = useMemo(
|
const atUri = useMemo(
|
||||||
|
|||||||
@@ -1,13 +1,12 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import {CenteredView} from '../util/Views'
|
import {CenteredView} from '../util/Views'
|
||||||
import {ActivityIndicator, RefreshControl, StyleSheet, View} from 'react-native'
|
import {ActivityIndicator, StyleSheet, View} from 'react-native'
|
||||||
import {FeedItem} from './FeedItem'
|
import {FeedItem} from './FeedItem'
|
||||||
import {NotificationFeedLoadingPlaceholder} from '../util/LoadingPlaceholder'
|
import {NotificationFeedLoadingPlaceholder} from '../util/LoadingPlaceholder'
|
||||||
import {ErrorMessage} from '../util/error/ErrorMessage'
|
import {ErrorMessage} from '../util/error/ErrorMessage'
|
||||||
import {LoadMoreRetryBtn} from '../util/LoadMoreRetryBtn'
|
import {LoadMoreRetryBtn} from '../util/LoadMoreRetryBtn'
|
||||||
import {EmptyState} from '../util/EmptyState'
|
import {EmptyState} from '../util/EmptyState'
|
||||||
import {s} from 'lib/styles'
|
import {s} from 'lib/styles'
|
||||||
import {usePalette} from 'lib/hooks/usePalette'
|
|
||||||
import {useNotificationFeedQuery} from '#/state/queries/notifications/feed'
|
import {useNotificationFeedQuery} from '#/state/queries/notifications/feed'
|
||||||
import {useUnreadNotificationsApi} from '#/state/queries/notifications/unread'
|
import {useUnreadNotificationsApi} from '#/state/queries/notifications/unread'
|
||||||
import {logger} from '#/logger'
|
import {logger} from '#/logger'
|
||||||
@@ -30,7 +29,6 @@ export function Feed({
|
|||||||
onScrolledDownChange: (isScrolledDown: boolean) => void
|
onScrolledDownChange: (isScrolledDown: boolean) => void
|
||||||
ListHeaderComponent?: () => JSX.Element
|
ListHeaderComponent?: () => JSX.Element
|
||||||
}) {
|
}) {
|
||||||
const pal = usePalette('default')
|
|
||||||
const [isPTRing, setIsPTRing] = React.useState(false)
|
const [isPTRing, setIsPTRing] = React.useState(false)
|
||||||
|
|
||||||
const moderationOpts = useModerationOpts()
|
const moderationOpts = useModerationOpts()
|
||||||
@@ -152,14 +150,8 @@ export function Feed({
|
|||||||
renderItem={renderItem}
|
renderItem={renderItem}
|
||||||
ListHeaderComponent={ListHeaderComponent}
|
ListHeaderComponent={ListHeaderComponent}
|
||||||
ListFooterComponent={FeedFooter}
|
ListFooterComponent={FeedFooter}
|
||||||
refreshControl={
|
|
||||||
<RefreshControl
|
|
||||||
refreshing={isPTRing}
|
refreshing={isPTRing}
|
||||||
onRefresh={onRefresh}
|
onRefresh={onRefresh}
|
||||||
tintColor={pal.colors.text}
|
|
||||||
titleColor={pal.colors.text}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
onEndReached={onEndReached}
|
onEndReached={onEndReached}
|
||||||
onEndReachedThreshold={0.6}
|
onEndReachedThreshold={0.6}
|
||||||
onScrolledDownChange={onScrolledDownChange}
|
onScrolledDownChange={onScrolledDownChange}
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ import {TimeElapsed} from '../util/TimeElapsed'
|
|||||||
import {isWeb} from 'platform/detection'
|
import {isWeb} from 'platform/detection'
|
||||||
import {Trans, msg} from '@lingui/macro'
|
import {Trans, msg} from '@lingui/macro'
|
||||||
import {useLingui} from '@lingui/react'
|
import {useLingui} from '@lingui/react'
|
||||||
|
import {FeedSourceCard} from '../feeds/FeedSourceCard'
|
||||||
|
|
||||||
const MAX_AUTHORS = 5
|
const MAX_AUTHORS = 5
|
||||||
|
|
||||||
@@ -112,7 +113,7 @@ let FeedItem = ({
|
|||||||
]
|
]
|
||||||
}, [item, moderationOpts])
|
}, [item, moderationOpts])
|
||||||
|
|
||||||
if (item.subjectUri && !item.subject) {
|
if (item.subjectUri && !item.subject && item.type !== 'feedgen-like') {
|
||||||
// don't render anything if the target post was deleted or unfindable
|
// don't render anything if the target post was deleted or unfindable
|
||||||
return <View />
|
return <View />
|
||||||
}
|
}
|
||||||
@@ -166,7 +167,7 @@ let FeedItem = ({
|
|||||||
iconStyle = [s.blue3 as FontAwesomeIconStyle]
|
iconStyle = [s.blue3 as FontAwesomeIconStyle]
|
||||||
} else if (item.type === 'feedgen-like') {
|
} else if (item.type === 'feedgen-like') {
|
||||||
action = `liked your custom feed${
|
action = `liked your custom feed${
|
||||||
item.subjectUri ? ` '${new AtUri(item.subjectUri).rkey}}'` : ''
|
item.subjectUri ? ` '${new AtUri(item.subjectUri).rkey}'` : ''
|
||||||
}`
|
}`
|
||||||
icon = 'HeartIconSolid'
|
icon = 'HeartIconSolid'
|
||||||
iconStyle = [
|
iconStyle = [
|
||||||
@@ -256,6 +257,13 @@ let FeedItem = ({
|
|||||||
{item.type === 'post-like' || item.type === 'repost' ? (
|
{item.type === 'post-like' || item.type === 'repost' ? (
|
||||||
<AdditionalPostText post={item.subject} />
|
<AdditionalPostText post={item.subject} />
|
||||||
) : null}
|
) : null}
|
||||||
|
{item.type === 'feedgen-like' && item.subjectUri ? (
|
||||||
|
<FeedSourceCard
|
||||||
|
feedUri={item.subjectUri}
|
||||||
|
style={[pal.view, pal.border, styles.feedcard]}
|
||||||
|
showLikes
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
</View>
|
</View>
|
||||||
</Link>
|
</Link>
|
||||||
)
|
)
|
||||||
@@ -496,6 +504,12 @@ const styles = StyleSheet.create({
|
|||||||
marginLeft: 2,
|
marginLeft: 2,
|
||||||
opacity: 0.8,
|
opacity: 0.8,
|
||||||
},
|
},
|
||||||
|
feedcard: {
|
||||||
|
borderWidth: 1,
|
||||||
|
borderRadius: 8,
|
||||||
|
paddingVertical: 12,
|
||||||
|
marginTop: 6,
|
||||||
|
},
|
||||||
|
|
||||||
addedContainer: {
|
addedContainer: {
|
||||||
paddingTop: 4,
|
paddingTop: 4,
|
||||||
|
|||||||
@@ -1,18 +1,16 @@
|
|||||||
import React, {useCallback, useMemo, useState} from 'react'
|
import React, {useCallback, useMemo, useState} from 'react'
|
||||||
import {ActivityIndicator, RefreshControl, StyleSheet, View} from 'react-native'
|
import {ActivityIndicator, StyleSheet, View} from 'react-native'
|
||||||
import {AppBskyFeedGetLikes as GetLikes} from '@atproto/api'
|
import {AppBskyFeedGetLikes as GetLikes} from '@atproto/api'
|
||||||
import {CenteredView} from '../util/Views'
|
import {CenteredView} from '../util/Views'
|
||||||
import {List} from '../util/List'
|
import {List} from '../util/List'
|
||||||
import {ErrorMessage} from '../util/error/ErrorMessage'
|
import {ErrorMessage} from '../util/error/ErrorMessage'
|
||||||
import {ProfileCardWithFollowBtn} from '../profile/ProfileCard'
|
import {ProfileCardWithFollowBtn} from '../profile/ProfileCard'
|
||||||
import {usePalette} from 'lib/hooks/usePalette'
|
|
||||||
import {logger} from '#/logger'
|
import {logger} from '#/logger'
|
||||||
import {useResolveUriQuery} from '#/state/queries/resolve-uri'
|
import {useResolveUriQuery} from '#/state/queries/resolve-uri'
|
||||||
import {usePostLikedByQuery} from '#/state/queries/post-liked-by'
|
import {usePostLikedByQuery} from '#/state/queries/post-liked-by'
|
||||||
import {cleanError} from '#/lib/strings/errors'
|
import {cleanError} from '#/lib/strings/errors'
|
||||||
|
|
||||||
export function PostLikedBy({uri}: {uri: string}) {
|
export function PostLikedBy({uri}: {uri: string}) {
|
||||||
const pal = usePalette('default')
|
|
||||||
const [isPTRing, setIsPTRing] = useState(false)
|
const [isPTRing, setIsPTRing] = useState(false)
|
||||||
const {
|
const {
|
||||||
data: resolvedUri,
|
data: resolvedUri,
|
||||||
@@ -88,14 +86,8 @@ export function PostLikedBy({uri}: {uri: string}) {
|
|||||||
<List
|
<List
|
||||||
data={likes}
|
data={likes}
|
||||||
keyExtractor={item => item.actor.did}
|
keyExtractor={item => item.actor.did}
|
||||||
refreshControl={
|
|
||||||
<RefreshControl
|
|
||||||
refreshing={isPTRing}
|
refreshing={isPTRing}
|
||||||
onRefresh={onRefresh}
|
onRefresh={onRefresh}
|
||||||
tintColor={pal.colors.text}
|
|
||||||
titleColor={pal.colors.text}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
onEndReached={onEndReached}
|
onEndReached={onEndReached}
|
||||||
renderItem={renderItem}
|
renderItem={renderItem}
|
||||||
initialNumToRender={15}
|
initialNumToRender={15}
|
||||||
|
|||||||
@@ -1,18 +1,16 @@
|
|||||||
import React, {useMemo, useCallback, useState} from 'react'
|
import React, {useMemo, useCallback, useState} from 'react'
|
||||||
import {ActivityIndicator, RefreshControl, StyleSheet, View} from 'react-native'
|
import {ActivityIndicator, StyleSheet, View} from 'react-native'
|
||||||
import {AppBskyActorDefs as ActorDefs} from '@atproto/api'
|
import {AppBskyActorDefs as ActorDefs} from '@atproto/api'
|
||||||
import {CenteredView} from '../util/Views'
|
import {CenteredView} from '../util/Views'
|
||||||
import {List} from '../util/List'
|
import {List} from '../util/List'
|
||||||
import {ProfileCardWithFollowBtn} from '../profile/ProfileCard'
|
import {ProfileCardWithFollowBtn} from '../profile/ProfileCard'
|
||||||
import {ErrorMessage} from '../util/error/ErrorMessage'
|
import {ErrorMessage} from '../util/error/ErrorMessage'
|
||||||
import {usePalette} from 'lib/hooks/usePalette'
|
|
||||||
import {logger} from '#/logger'
|
import {logger} from '#/logger'
|
||||||
import {useResolveUriQuery} from '#/state/queries/resolve-uri'
|
import {useResolveUriQuery} from '#/state/queries/resolve-uri'
|
||||||
import {usePostRepostedByQuery} from '#/state/queries/post-reposted-by'
|
import {usePostRepostedByQuery} from '#/state/queries/post-reposted-by'
|
||||||
import {cleanError} from '#/lib/strings/errors'
|
import {cleanError} from '#/lib/strings/errors'
|
||||||
|
|
||||||
export function PostRepostedBy({uri}: {uri: string}) {
|
export function PostRepostedBy({uri}: {uri: string}) {
|
||||||
const pal = usePalette('default')
|
|
||||||
const [isPTRing, setIsPTRing] = useState(false)
|
const [isPTRing, setIsPTRing] = useState(false)
|
||||||
const {
|
const {
|
||||||
data: resolvedUri,
|
data: resolvedUri,
|
||||||
@@ -89,14 +87,8 @@ export function PostRepostedBy({uri}: {uri: string}) {
|
|||||||
<List
|
<List
|
||||||
data={repostedBy}
|
data={repostedBy}
|
||||||
keyExtractor={item => item.did}
|
keyExtractor={item => item.did}
|
||||||
refreshControl={
|
|
||||||
<RefreshControl
|
|
||||||
refreshing={isPTRing}
|
refreshing={isPTRing}
|
||||||
onRefresh={onRefresh}
|
onRefresh={onRefresh}
|
||||||
tintColor={pal.colors.text}
|
|
||||||
titleColor={pal.colors.text}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
onEndReached={onEndReached}
|
onEndReached={onEndReached}
|
||||||
renderItem={renderItem}
|
renderItem={renderItem}
|
||||||
initialNumToRender={15}
|
initialNumToRender={15}
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import React, {useEffect, useRef} from 'react'
|
|||||||
import {
|
import {
|
||||||
ActivityIndicator,
|
ActivityIndicator,
|
||||||
Pressable,
|
Pressable,
|
||||||
RefreshControl,
|
|
||||||
StyleSheet,
|
StyleSheet,
|
||||||
TouchableOpacity,
|
TouchableOpacity,
|
||||||
View,
|
View,
|
||||||
@@ -349,14 +348,8 @@ function PostThreadLoaded({
|
|||||||
}
|
}
|
||||||
keyExtractor={item => item._reactKey}
|
keyExtractor={item => item._reactKey}
|
||||||
renderItem={renderItem}
|
renderItem={renderItem}
|
||||||
refreshControl={
|
|
||||||
<RefreshControl
|
|
||||||
refreshing={isPTRing}
|
refreshing={isPTRing}
|
||||||
onRefresh={onPTR}
|
onRefresh={onPTR}
|
||||||
tintColor={pal.colors.text}
|
|
||||||
titleColor={pal.colors.text}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
onContentSizeChange={onContentSizeChange}
|
onContentSizeChange={onContentSizeChange}
|
||||||
style={s.hContentRegion}
|
style={s.hContentRegion}
|
||||||
// @ts-ignore our .web version only -prf
|
// @ts-ignore our .web version only -prf
|
||||||
|
|||||||
@@ -5,9 +5,9 @@ import {
|
|||||||
AppBskyFeedDefs,
|
AppBskyFeedDefs,
|
||||||
AppBskyFeedPost,
|
AppBskyFeedPost,
|
||||||
RichText as RichTextAPI,
|
RichText as RichTextAPI,
|
||||||
moderatePost,
|
|
||||||
PostModeration,
|
PostModeration,
|
||||||
} from '@atproto/api'
|
} from '@atproto/api'
|
||||||
|
import {moderatePost_wrapped as moderatePost} from '#/lib/moderatePost_wrapped'
|
||||||
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
||||||
import {Link, TextLink} from '../util/Link'
|
import {Link, TextLink} from '../util/Link'
|
||||||
import {RichText} from '../util/text/RichText'
|
import {RichText} from '../util/text/RichText'
|
||||||
@@ -42,7 +42,6 @@ import {useComposerControls} from '#/state/shell/composer'
|
|||||||
import {useModerationOpts} from '#/state/queries/preferences'
|
import {useModerationOpts} from '#/state/queries/preferences'
|
||||||
import {Shadow, usePostShadow, POST_TOMBSTONE} from '#/state/cache/post-shadow'
|
import {Shadow, usePostShadow, POST_TOMBSTONE} from '#/state/cache/post-shadow'
|
||||||
import {ThreadPost} from '#/state/queries/post-thread'
|
import {ThreadPost} from '#/state/queries/post-thread'
|
||||||
import {LabelInfo} from '../util/moderation/LabelInfo'
|
|
||||||
import {useSession} from '#/state/session'
|
import {useSession} from '#/state/session'
|
||||||
import {WhoCanReply} from '../threadgate/WhoCanReply'
|
import {WhoCanReply} from '../threadgate/WhoCanReply'
|
||||||
|
|
||||||
@@ -187,9 +186,9 @@ let PostThreadItemLoaded = ({
|
|||||||
return makeProfileLink(post.author, 'post', urip.rkey, 'reposted-by')
|
return makeProfileLink(post.author, 'post', urip.rkey, 'reposted-by')
|
||||||
}, [post.uri, post.author])
|
}, [post.uri, post.author])
|
||||||
const repostsTitle = 'Reposts of this post'
|
const repostsTitle = 'Reposts of this post'
|
||||||
const isSelfLabeledPost =
|
const isModeratedPost =
|
||||||
moderation.decisions.post.cause?.type === 'label' &&
|
moderation.decisions.post.cause?.type === 'label' &&
|
||||||
moderation.decisions.post.cause.label.src === currentAccount?.did
|
moderation.decisions.post.cause.label.src !== currentAccount?.did
|
||||||
|
|
||||||
const translatorUrl = getTranslatorLink(
|
const translatorUrl = getTranslatorLink(
|
||||||
record?.text || '',
|
record?.text || '',
|
||||||
@@ -335,6 +334,9 @@ let PostThreadItemLoaded = ({
|
|||||||
postCid={post.cid}
|
postCid={post.cid}
|
||||||
postUri={post.uri}
|
postUri={post.uri}
|
||||||
record={record}
|
record={record}
|
||||||
|
showAppealLabelItem={
|
||||||
|
post.author.did === currentAccount?.did && isModeratedPost
|
||||||
|
}
|
||||||
style={{
|
style={{
|
||||||
paddingVertical: 6,
|
paddingVertical: 6,
|
||||||
paddingHorizontal: 10,
|
paddingHorizontal: 10,
|
||||||
@@ -354,13 +356,6 @@ let PostThreadItemLoaded = ({
|
|||||||
includeMute
|
includeMute
|
||||||
style={styles.alert}
|
style={styles.alert}
|
||||||
/>
|
/>
|
||||||
{post.author.did === currentAccount?.did && !isSelfLabeledPost ? (
|
|
||||||
<LabelInfo
|
|
||||||
details={{uri: post.uri, cid: post.cid}}
|
|
||||||
labels={post.labels}
|
|
||||||
style={{marginBottom: 8}}
|
|
||||||
/>
|
|
||||||
) : null}
|
|
||||||
{richText?.text ? (
|
{richText?.text ? (
|
||||||
<View
|
<View
|
||||||
style={[
|
style={[
|
||||||
@@ -544,6 +539,7 @@ let PostThreadItemLoaded = ({
|
|||||||
timestamp={post.indexedAt}
|
timestamp={post.indexedAt}
|
||||||
postHref={postHref}
|
postHref={postHref}
|
||||||
showAvatar={isThreadedChild}
|
showAvatar={isThreadedChild}
|
||||||
|
avatarModeration={moderation.avatar}
|
||||||
avatarSize={28}
|
avatarSize={28}
|
||||||
displayNameType="md-bold"
|
displayNameType="md-bold"
|
||||||
displayNameStyle={isThreadedChild && s.ml2}
|
displayNameStyle={isThreadedChild && s.ml2}
|
||||||
|
|||||||
@@ -4,10 +4,10 @@ import {
|
|||||||
AppBskyFeedDefs,
|
AppBskyFeedDefs,
|
||||||
AppBskyFeedPost,
|
AppBskyFeedPost,
|
||||||
AtUri,
|
AtUri,
|
||||||
moderatePost,
|
|
||||||
PostModeration,
|
PostModeration,
|
||||||
RichText as RichTextAPI,
|
RichText as RichTextAPI,
|
||||||
} from '@atproto/api'
|
} from '@atproto/api'
|
||||||
|
import {moderatePost_wrapped as moderatePost} from '#/lib/moderatePost_wrapped'
|
||||||
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
||||||
import {Link, TextLink} from '../util/Link'
|
import {Link, TextLink} from '../util/Link'
|
||||||
import {UserInfoText} from '../util/UserInfoText'
|
import {UserInfoText} from '../util/UserInfoText'
|
||||||
@@ -221,6 +221,7 @@ const styles = StyleSheet.create({
|
|||||||
paddingBottom: 5,
|
paddingBottom: 5,
|
||||||
paddingLeft: 10,
|
paddingLeft: 10,
|
||||||
borderTopWidth: 1,
|
borderTopWidth: 1,
|
||||||
|
// @ts-ignore web only -prf
|
||||||
cursor: 'pointer',
|
cursor: 'pointer',
|
||||||
},
|
},
|
||||||
layout: {
|
layout: {
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import {
|
|||||||
ActivityIndicator,
|
ActivityIndicator,
|
||||||
AppState,
|
AppState,
|
||||||
Dimensions,
|
Dimensions,
|
||||||
RefreshControl,
|
|
||||||
StyleProp,
|
StyleProp,
|
||||||
StyleSheet,
|
StyleSheet,
|
||||||
View,
|
View,
|
||||||
@@ -16,7 +15,6 @@ import {FeedErrorMessage} from './FeedErrorMessage'
|
|||||||
import {FeedSlice} from './FeedSlice'
|
import {FeedSlice} from './FeedSlice'
|
||||||
import {LoadMoreRetryBtn} from '../util/LoadMoreRetryBtn'
|
import {LoadMoreRetryBtn} from '../util/LoadMoreRetryBtn'
|
||||||
import {useAnalytics} from 'lib/analytics/analytics'
|
import {useAnalytics} from 'lib/analytics/analytics'
|
||||||
import {usePalette} from 'lib/hooks/usePalette'
|
|
||||||
import {useTheme} from 'lib/ThemeContext'
|
import {useTheme} from 'lib/ThemeContext'
|
||||||
import {logger} from '#/logger'
|
import {logger} from '#/logger'
|
||||||
import {
|
import {
|
||||||
@@ -74,7 +72,6 @@ let Feed = ({
|
|||||||
ListHeaderComponent?: () => JSX.Element
|
ListHeaderComponent?: () => JSX.Element
|
||||||
extraData?: any
|
extraData?: any
|
||||||
}): React.ReactNode => {
|
}): React.ReactNode => {
|
||||||
const pal = usePalette('default')
|
|
||||||
const theme = useTheme()
|
const theme = useTheme()
|
||||||
const {track} = useAnalytics()
|
const {track} = useAnalytics()
|
||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
@@ -98,10 +95,13 @@ let Feed = ({
|
|||||||
isFetchingNextPage,
|
isFetchingNextPage,
|
||||||
fetchNextPage,
|
fetchNextPage,
|
||||||
} = usePostFeedQuery(feed, feedParams, opts)
|
} = usePostFeedQuery(feed, feedParams, opts)
|
||||||
const isEmpty = !isFetching && !data?.pages[0]?.slices.length
|
|
||||||
if (data?.pages[0]) {
|
if (data?.pages[0]) {
|
||||||
lastFetchRef.current = data?.pages[0].fetchedAt
|
lastFetchRef.current = data?.pages[0].fetchedAt
|
||||||
}
|
}
|
||||||
|
const isEmpty = React.useMemo(
|
||||||
|
() => !isFetching && !data?.pages?.some(page => page.slices.length),
|
||||||
|
[isFetching, data],
|
||||||
|
)
|
||||||
|
|
||||||
const checkForNew = React.useCallback(async () => {
|
const checkForNew = React.useCallback(async () => {
|
||||||
if (!data?.pages[0] || isFetching || !onHasNew || !enabled) {
|
if (!data?.pages[0] || isFetching || !onHasNew || !enabled) {
|
||||||
@@ -294,25 +294,17 @@ let Feed = ({
|
|||||||
renderItem={renderItem}
|
renderItem={renderItem}
|
||||||
ListFooterComponent={FeedFooter}
|
ListFooterComponent={FeedFooter}
|
||||||
ListHeaderComponent={ListHeaderComponent}
|
ListHeaderComponent={ListHeaderComponent}
|
||||||
refreshControl={
|
|
||||||
<RefreshControl
|
|
||||||
refreshing={isPTRing}
|
refreshing={isPTRing}
|
||||||
onRefresh={onRefresh}
|
onRefresh={onRefresh}
|
||||||
tintColor={pal.colors.text}
|
headerOffset={headerOffset}
|
||||||
titleColor={pal.colors.text}
|
|
||||||
progressViewOffset={headerOffset}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
contentContainerStyle={{
|
contentContainerStyle={{
|
||||||
minHeight: Dimensions.get('window').height * 1.5,
|
minHeight: Dimensions.get('window').height * 1.5,
|
||||||
}}
|
}}
|
||||||
style={{paddingTop: headerOffset}}
|
|
||||||
onScrolledDownChange={onScrolledDownChange}
|
onScrolledDownChange={onScrolledDownChange}
|
||||||
indicatorStyle={theme.colorScheme === 'dark' ? 'white' : 'black'}
|
indicatorStyle={theme.colorScheme === 'dark' ? 'white' : 'black'}
|
||||||
onEndReached={onEndReached}
|
onEndReached={onEndReached}
|
||||||
onEndReachedThreshold={2} // number of posts left to trigger load more
|
onEndReachedThreshold={2} // number of posts left to trigger load more
|
||||||
removeClippedSubviews={true}
|
removeClippedSubviews={true}
|
||||||
contentOffset={{x: 0, y: headerOffset * -1}}
|
|
||||||
extraData={extraData}
|
extraData={extraData}
|
||||||
// @ts-ignore our .web version only -prf
|
// @ts-ignore our .web version only -prf
|
||||||
desktopFixedHeight={
|
desktopFixedHeight={
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ import {countLines} from 'lib/strings/helpers'
|
|||||||
import {useComposerControls} from '#/state/shell/composer'
|
import {useComposerControls} from '#/state/shell/composer'
|
||||||
import {Shadow, usePostShadow, POST_TOMBSTONE} from '#/state/cache/post-shadow'
|
import {Shadow, usePostShadow, POST_TOMBSTONE} from '#/state/cache/post-shadow'
|
||||||
import {FeedNameText} from '../util/FeedInfoText'
|
import {FeedNameText} from '../util/FeedInfoText'
|
||||||
|
import {useSession} from '#/state/session'
|
||||||
|
|
||||||
export function FeedItem({
|
export function FeedItem({
|
||||||
post,
|
post,
|
||||||
@@ -102,10 +103,14 @@ let FeedItemInner = ({
|
|||||||
}): React.ReactNode => {
|
}): React.ReactNode => {
|
||||||
const {openComposer} = useComposerControls()
|
const {openComposer} = useComposerControls()
|
||||||
const pal = usePalette('default')
|
const pal = usePalette('default')
|
||||||
|
const {currentAccount} = useSession()
|
||||||
const href = useMemo(() => {
|
const href = useMemo(() => {
|
||||||
const urip = new AtUri(post.uri)
|
const urip = new AtUri(post.uri)
|
||||||
return makeProfileLink(post.author, 'post', urip.rkey)
|
return makeProfileLink(post.author, 'post', urip.rkey)
|
||||||
}, [post.uri, post.author])
|
}, [post.uri, post.author])
|
||||||
|
const isModeratedPost =
|
||||||
|
moderation.decisions.post.cause?.type === 'label' &&
|
||||||
|
moderation.decisions.post.cause.label.src !== currentAccount?.did
|
||||||
|
|
||||||
const replyAuthorDid = useMemo(() => {
|
const replyAuthorDid = useMemo(() => {
|
||||||
if (!record?.reply) {
|
if (!record?.reply) {
|
||||||
@@ -284,7 +289,14 @@ let FeedItemInner = ({
|
|||||||
postEmbed={post.embed}
|
postEmbed={post.embed}
|
||||||
postAuthor={post.author}
|
postAuthor={post.author}
|
||||||
/>
|
/>
|
||||||
<PostCtrls post={post} record={record} onPressReply={onPressReply} />
|
<PostCtrls
|
||||||
|
post={post}
|
||||||
|
record={record}
|
||||||
|
onPressReply={onPressReply}
|
||||||
|
showAppealLabelItem={
|
||||||
|
post.author.did === currentAccount?.did && isModeratedPost
|
||||||
|
}
|
||||||
|
/>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
</Link>
|
</Link>
|
||||||
@@ -364,6 +376,7 @@ const styles = StyleSheet.create({
|
|||||||
borderTopWidth: 1,
|
borderTopWidth: 1,
|
||||||
paddingLeft: 10,
|
paddingLeft: 10,
|
||||||
paddingRight: 15,
|
paddingRight: 15,
|
||||||
|
// @ts-ignore web only -prf
|
||||||
cursor: 'pointer',
|
cursor: 'pointer',
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ import {useSession} from '#/state/session'
|
|||||||
export function ProfileCard({
|
export function ProfileCard({
|
||||||
testID,
|
testID,
|
||||||
profile: profileUnshadowed,
|
profile: profileUnshadowed,
|
||||||
|
noModFilter,
|
||||||
noBg,
|
noBg,
|
||||||
noBorder,
|
noBorder,
|
||||||
followers,
|
followers,
|
||||||
@@ -35,6 +36,7 @@ export function ProfileCard({
|
|||||||
}: {
|
}: {
|
||||||
testID?: string
|
testID?: string
|
||||||
profile: AppBskyActorDefs.ProfileViewBasic
|
profile: AppBskyActorDefs.ProfileViewBasic
|
||||||
|
noModFilter?: boolean
|
||||||
noBg?: boolean
|
noBg?: boolean
|
||||||
noBorder?: boolean
|
noBorder?: boolean
|
||||||
followers?: AppBskyActorDefs.ProfileView[] | undefined
|
followers?: AppBskyActorDefs.ProfileView[] | undefined
|
||||||
@@ -50,7 +52,11 @@ export function ProfileCard({
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
const moderation = moderateProfile(profile, moderationOpts)
|
const moderation = moderateProfile(profile, moderationOpts)
|
||||||
if (moderation.account.filter) {
|
if (
|
||||||
|
!noModFilter &&
|
||||||
|
moderation.account.filter &&
|
||||||
|
moderation.account.cause?.type !== 'muted'
|
||||||
|
) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,18 +1,16 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import {ActivityIndicator, RefreshControl, StyleSheet, View} from 'react-native'
|
import {ActivityIndicator, StyleSheet, View} from 'react-native'
|
||||||
import {AppBskyActorDefs as ActorDefs} from '@atproto/api'
|
import {AppBskyActorDefs as ActorDefs} from '@atproto/api'
|
||||||
import {CenteredView} from '../util/Views'
|
import {CenteredView} from '../util/Views'
|
||||||
import {List} from '../util/List'
|
import {List} from '../util/List'
|
||||||
import {ErrorMessage} from '../util/error/ErrorMessage'
|
import {ErrorMessage} from '../util/error/ErrorMessage'
|
||||||
import {ProfileCardWithFollowBtn} from './ProfileCard'
|
import {ProfileCardWithFollowBtn} from './ProfileCard'
|
||||||
import {usePalette} from 'lib/hooks/usePalette'
|
|
||||||
import {useProfileFollowersQuery} from '#/state/queries/profile-followers'
|
import {useProfileFollowersQuery} from '#/state/queries/profile-followers'
|
||||||
import {useResolveDidQuery} from '#/state/queries/resolve-uri'
|
import {useResolveDidQuery} from '#/state/queries/resolve-uri'
|
||||||
import {logger} from '#/logger'
|
import {logger} from '#/logger'
|
||||||
import {cleanError} from '#/lib/strings/errors'
|
import {cleanError} from '#/lib/strings/errors'
|
||||||
|
|
||||||
export function ProfileFollowers({name}: {name: string}) {
|
export function ProfileFollowers({name}: {name: string}) {
|
||||||
const pal = usePalette('default')
|
|
||||||
const [isPTRing, setIsPTRing] = React.useState(false)
|
const [isPTRing, setIsPTRing] = React.useState(false)
|
||||||
const {
|
const {
|
||||||
data: resolvedDid,
|
data: resolvedDid,
|
||||||
@@ -90,14 +88,8 @@ export function ProfileFollowers({name}: {name: string}) {
|
|||||||
<List
|
<List
|
||||||
data={followers}
|
data={followers}
|
||||||
keyExtractor={item => item.did}
|
keyExtractor={item => item.did}
|
||||||
refreshControl={
|
|
||||||
<RefreshControl
|
|
||||||
refreshing={isPTRing}
|
refreshing={isPTRing}
|
||||||
onRefresh={onRefresh}
|
onRefresh={onRefresh}
|
||||||
tintColor={pal.colors.text}
|
|
||||||
titleColor={pal.colors.text}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
onEndReached={onEndReached}
|
onEndReached={onEndReached}
|
||||||
renderItem={renderItem}
|
renderItem={renderItem}
|
||||||
initialNumToRender={15}
|
initialNumToRender={15}
|
||||||
|
|||||||
@@ -1,18 +1,16 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import {ActivityIndicator, RefreshControl, StyleSheet, View} from 'react-native'
|
import {ActivityIndicator, StyleSheet, View} from 'react-native'
|
||||||
import {AppBskyActorDefs as ActorDefs} from '@atproto/api'
|
import {AppBskyActorDefs as ActorDefs} from '@atproto/api'
|
||||||
import {CenteredView} from '../util/Views'
|
import {CenteredView} from '../util/Views'
|
||||||
import {List} from '../util/List'
|
import {List} from '../util/List'
|
||||||
import {ErrorMessage} from '../util/error/ErrorMessage'
|
import {ErrorMessage} from '../util/error/ErrorMessage'
|
||||||
import {ProfileCardWithFollowBtn} from './ProfileCard'
|
import {ProfileCardWithFollowBtn} from './ProfileCard'
|
||||||
import {usePalette} from 'lib/hooks/usePalette'
|
|
||||||
import {useProfileFollowsQuery} from '#/state/queries/profile-follows'
|
import {useProfileFollowsQuery} from '#/state/queries/profile-follows'
|
||||||
import {useResolveDidQuery} from '#/state/queries/resolve-uri'
|
import {useResolveDidQuery} from '#/state/queries/resolve-uri'
|
||||||
import {logger} from '#/logger'
|
import {logger} from '#/logger'
|
||||||
import {cleanError} from '#/lib/strings/errors'
|
import {cleanError} from '#/lib/strings/errors'
|
||||||
|
|
||||||
export function ProfileFollows({name}: {name: string}) {
|
export function ProfileFollows({name}: {name: string}) {
|
||||||
const pal = usePalette('default')
|
|
||||||
const [isPTRing, setIsPTRing] = React.useState(false)
|
const [isPTRing, setIsPTRing] = React.useState(false)
|
||||||
const {
|
const {
|
||||||
data: resolvedDid,
|
data: resolvedDid,
|
||||||
@@ -90,14 +88,8 @@ export function ProfileFollows({name}: {name: string}) {
|
|||||||
<List
|
<List
|
||||||
data={follows}
|
data={follows}
|
||||||
keyExtractor={item => item.did}
|
keyExtractor={item => item.did}
|
||||||
refreshControl={
|
|
||||||
<RefreshControl
|
|
||||||
refreshing={isPTRing}
|
refreshing={isPTRing}
|
||||||
onRefresh={onRefresh}
|
onRefresh={onRefresh}
|
||||||
tintColor={pal.colors.text}
|
|
||||||
titleColor={pal.colors.text}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
onEndReached={onEndReached}
|
onEndReached={onEndReached}
|
||||||
renderItem={renderItem}
|
renderItem={renderItem}
|
||||||
initialNumToRender={15}
|
initialNumToRender={15}
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import {StyleSheet, View, ViewProps} from 'react-native'
|
||||||
|
import {addStyle} from 'lib/styles'
|
||||||
|
|
||||||
|
type BlurViewProps = ViewProps & {
|
||||||
|
blurType?: 'dark' | 'light'
|
||||||
|
blurAmount?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export const BlurView = ({
|
||||||
|
style,
|
||||||
|
blurType,
|
||||||
|
...props
|
||||||
|
}: React.PropsWithChildren<BlurViewProps>) => {
|
||||||
|
if (blurType === 'dark') {
|
||||||
|
style = addStyle(style, styles.dark)
|
||||||
|
} else {
|
||||||
|
style = addStyle(style, styles.light)
|
||||||
|
}
|
||||||
|
return <View style={style} {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
dark: {
|
||||||
|
backgroundColor: '#0008',
|
||||||
|
},
|
||||||
|
light: {
|
||||||
|
backgroundColor: '#fff8',
|
||||||
|
},
|
||||||
|
})
|
||||||
@@ -30,6 +30,7 @@ export function H1({children}: React.PropsWithChildren<{}>) {
|
|||||||
const styles = useStyles()
|
const styles = useStyles()
|
||||||
const pal = usePalette('default')
|
const pal = usePalette('default')
|
||||||
const typography = useTheme().typography['title-xl']
|
const typography = useTheme().typography['title-xl']
|
||||||
|
// @ts-ignore Expo's TextStyle definition seems to have gotten away from RN's -prf
|
||||||
return <ExpoH1 style={[typography, pal.text, styles.h1]}>{children}</ExpoH1>
|
return <ExpoH1 style={[typography, pal.text, styles.h1]}>{children}</ExpoH1>
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -37,6 +38,7 @@ export function H2({children}: React.PropsWithChildren<{}>) {
|
|||||||
const styles = useStyles()
|
const styles = useStyles()
|
||||||
const pal = usePalette('default')
|
const pal = usePalette('default')
|
||||||
const typography = useTheme().typography['title-lg']
|
const typography = useTheme().typography['title-lg']
|
||||||
|
// @ts-ignore Expo's TextStyle definition seems to have gotten away from RN's -prf
|
||||||
return <ExpoH2 style={[typography, pal.text, styles.h2]}>{children}</ExpoH2>
|
return <ExpoH2 style={[typography, pal.text, styles.h2]}>{children}</ExpoH2>
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -44,6 +46,7 @@ export function H3({children}: React.PropsWithChildren<{}>) {
|
|||||||
const styles = useStyles()
|
const styles = useStyles()
|
||||||
const pal = usePalette('default')
|
const pal = usePalette('default')
|
||||||
const typography = useTheme().typography.title
|
const typography = useTheme().typography.title
|
||||||
|
// @ts-ignore Expo's TextStyle definition seems to have gotten away from RN's -prf
|
||||||
return <ExpoH3 style={[typography, pal.text, styles.h3]}>{children}</ExpoH3>
|
return <ExpoH3 style={[typography, pal.text, styles.h3]}>{children}</ExpoH3>
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -51,6 +54,7 @@ export function H4({children}: React.PropsWithChildren<{}>) {
|
|||||||
const styles = useStyles()
|
const styles = useStyles()
|
||||||
const pal = usePalette('default')
|
const pal = usePalette('default')
|
||||||
const typography = useTheme().typography['title-sm']
|
const typography = useTheme().typography['title-sm']
|
||||||
|
// @ts-ignore Expo's TextStyle definition seems to have gotten away from RN's -prf
|
||||||
return <ExpoH4 style={[typography, pal.text, styles.h4]}>{children}</ExpoH4>
|
return <ExpoH4 style={[typography, pal.text, styles.h4]}>{children}</ExpoH4>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,27 +1,42 @@
|
|||||||
import React, {memo, startTransition} from 'react'
|
import React, {memo, startTransition} from 'react'
|
||||||
import {FlatListProps} from 'react-native'
|
import {FlatListProps, RefreshControl} from 'react-native'
|
||||||
import {FlatList_INTERNAL} from './Views'
|
import {FlatList_INTERNAL} from './Views'
|
||||||
|
import {addStyle} from 'lib/styles'
|
||||||
import {useScrollHandlers} from '#/lib/ScrollContext'
|
import {useScrollHandlers} from '#/lib/ScrollContext'
|
||||||
import {runOnJS, useSharedValue} from 'react-native-reanimated'
|
import {runOnJS, useSharedValue} from 'react-native-reanimated'
|
||||||
import {useAnimatedScrollHandler} from '#/lib/hooks/useAnimatedScrollHandler_FIXED'
|
import {useAnimatedScrollHandler} from '#/lib/hooks/useAnimatedScrollHandler_FIXED'
|
||||||
|
import {usePalette} from '#/lib/hooks/usePalette'
|
||||||
|
|
||||||
export type ListMethods = FlatList_INTERNAL
|
export type ListMethods = FlatList_INTERNAL
|
||||||
export type ListProps<ItemT> = Omit<
|
export type ListProps<ItemT> = Omit<
|
||||||
FlatListProps<ItemT>,
|
FlatListProps<ItemT>,
|
||||||
'onScroll' // Use ScrollContext instead.
|
| 'onScroll' // Use ScrollContext instead.
|
||||||
|
| 'refreshControl' // Pass refreshing and/or onRefresh instead.
|
||||||
|
| 'contentOffset' // Pass headerOffset instead.
|
||||||
> & {
|
> & {
|
||||||
onScrolledDownChange?: (isScrolledDown: boolean) => void
|
onScrolledDownChange?: (isScrolledDown: boolean) => void
|
||||||
|
headerOffset?: number
|
||||||
|
refreshing?: boolean
|
||||||
|
onRefresh?: () => void
|
||||||
}
|
}
|
||||||
export type ListRef = React.MutableRefObject<FlatList_INTERNAL | null>
|
export type ListRef = React.MutableRefObject<FlatList_INTERNAL | null>
|
||||||
|
|
||||||
const SCROLLED_DOWN_LIMIT = 200
|
const SCROLLED_DOWN_LIMIT = 200
|
||||||
|
|
||||||
function ListImpl<ItemT>(
|
function ListImpl<ItemT>(
|
||||||
{onScrolledDownChange, ...props}: ListProps<ItemT>,
|
{
|
||||||
|
onScrolledDownChange,
|
||||||
|
refreshing,
|
||||||
|
onRefresh,
|
||||||
|
headerOffset,
|
||||||
|
style,
|
||||||
|
...props
|
||||||
|
}: ListProps<ItemT>,
|
||||||
ref: React.Ref<ListMethods>,
|
ref: React.Ref<ListMethods>,
|
||||||
) {
|
) {
|
||||||
const isScrolledDown = useSharedValue(false)
|
const isScrolledDown = useSharedValue(false)
|
||||||
const contextScrollHandlers = useScrollHandlers()
|
const contextScrollHandlers = useScrollHandlers()
|
||||||
|
const pal = usePalette('default')
|
||||||
|
|
||||||
function handleScrolledDownChange(didScrollDown: boolean) {
|
function handleScrolledDownChange(didScrollDown: boolean) {
|
||||||
startTransition(() => {
|
startTransition(() => {
|
||||||
@@ -49,11 +64,36 @@ function ListImpl<ItemT>(
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
let refreshControl
|
||||||
|
if (refreshing !== undefined || onRefresh !== undefined) {
|
||||||
|
refreshControl = (
|
||||||
|
<RefreshControl
|
||||||
|
refreshing={refreshing ?? false}
|
||||||
|
onRefresh={onRefresh}
|
||||||
|
tintColor={pal.colors.text}
|
||||||
|
titleColor={pal.colors.text}
|
||||||
|
progressViewOffset={headerOffset}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
let contentOffset
|
||||||
|
if (headerOffset != null) {
|
||||||
|
style = addStyle(style, {
|
||||||
|
paddingTop: headerOffset,
|
||||||
|
})
|
||||||
|
contentOffset = {x: 0, y: headerOffset * -1}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FlatList_INTERNAL
|
<FlatList_INTERNAL
|
||||||
{...props}
|
{...props}
|
||||||
|
scrollIndicatorInsets={{right: 1}}
|
||||||
|
contentOffset={contentOffset}
|
||||||
|
refreshControl={refreshControl}
|
||||||
onScroll={scrollHandler}
|
onScroll={scrollHandler}
|
||||||
scrollEventThrottle={1}
|
scrollEventThrottle={1}
|
||||||
|
style={style}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import {sanitizeHandle} from 'lib/strings/handles'
|
|||||||
import {isAndroid} from 'platform/detection'
|
import {isAndroid} from 'platform/detection'
|
||||||
import {TimeElapsed} from './TimeElapsed'
|
import {TimeElapsed} from './TimeElapsed'
|
||||||
import {makeProfileLink} from 'lib/routes/links'
|
import {makeProfileLink} from 'lib/routes/links'
|
||||||
|
import {ModerationUI} from '@atproto/api'
|
||||||
|
|
||||||
interface PostMetaOpts {
|
interface PostMetaOpts {
|
||||||
author: {
|
author: {
|
||||||
@@ -23,6 +24,7 @@ interface PostMetaOpts {
|
|||||||
postHref: string
|
postHref: string
|
||||||
timestamp: string
|
timestamp: string
|
||||||
showAvatar?: boolean
|
showAvatar?: boolean
|
||||||
|
avatarModeration?: ModerationUI
|
||||||
avatarSize?: number
|
avatarSize?: number
|
||||||
displayNameType?: TypographyVariant
|
displayNameType?: TypographyVariant
|
||||||
displayNameStyle?: StyleProp<TextStyle>
|
displayNameStyle?: StyleProp<TextStyle>
|
||||||
@@ -41,7 +43,7 @@ let PostMeta = (opts: PostMetaOpts): React.ReactNode => {
|
|||||||
<UserAvatar
|
<UserAvatar
|
||||||
avatar={opts.author.avatar}
|
avatar={opts.author.avatar}
|
||||||
size={opts.avatarSize || 16}
|
size={opts.avatarSize || 16}
|
||||||
// TODO moderation
|
moderation={opts.avatarModeration}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
|
|||||||