diff options
Diffstat (limited to 'src/lib')
-rw-r--r-- | src/lib/api/feed/custom.ts | 27 | ||||
-rw-r--r-- | src/lib/api/feed/home.ts | 89 | ||||
-rw-r--r-- | src/lib/api/feed/merge.ts | 17 | ||||
-rw-r--r-- | src/lib/batchedUpdates.web.ts | 1 | ||||
-rw-r--r-- | src/lib/country-codes.ts | 256 | ||||
-rw-r--r-- | src/lib/hooks/useWebBodyScrollLock.ts | 28 | ||||
-rw-r--r-- | src/lib/hooks/useWebScrollRestoration.native.ts | 3 | ||||
-rw-r--r-- | src/lib/hooks/useWebScrollRestoration.ts | 52 | ||||
-rw-r--r-- | src/lib/link-meta/bsky.ts | 13 | ||||
-rw-r--r-- | src/lib/strings/embed-player.ts | 6 | ||||
-rw-r--r-- | src/lib/strings/rich-text-helpers.ts | 4 | ||||
-rw-r--r-- | src/lib/styles.ts | 4 | ||||
-rw-r--r-- | src/lib/themes.ts | 110 |
13 files changed, 530 insertions, 80 deletions
diff --git a/src/lib/api/feed/custom.ts b/src/lib/api/feed/custom.ts index 94cbff130..41c5367e5 100644 --- a/src/lib/api/feed/custom.ts +++ b/src/lib/api/feed/custom.ts @@ -4,15 +4,20 @@ import { } from '@atproto/api' import {FeedAPI, FeedAPIResponse} from './types' import {getAgent} from '#/state/session' +import {getContentLanguages} from '#/state/preferences/languages' export class CustomFeedAPI implements FeedAPI { constructor(public params: GetCustomFeed.QueryParams) {} async peekLatest(): Promise<AppBskyFeedDefs.FeedViewPost> { - const res = await getAgent().app.bsky.feed.getFeed({ - ...this.params, - limit: 1, - }) + const contentLangs = getContentLanguages().join(',') + const res = await getAgent().app.bsky.feed.getFeed( + { + ...this.params, + limit: 1, + }, + {headers: {'Accept-Language': contentLangs}}, + ) return res.data.feed[0] } @@ -23,11 +28,15 @@ export class CustomFeedAPI implements FeedAPI { cursor: string | undefined limit: number }): Promise<FeedAPIResponse> { - const res = await getAgent().app.bsky.feed.getFeed({ - ...this.params, - cursor, - limit, - }) + const contentLangs = getContentLanguages().join(',') + const res = await getAgent().app.bsky.feed.getFeed( + { + ...this.params, + cursor, + limit, + }, + {headers: {'Accept-Language': contentLangs}}, + ) if (res.success) { // NOTE // some custom feeds fail to enforce the pagination limit diff --git a/src/lib/api/feed/home.ts b/src/lib/api/feed/home.ts new file mode 100644 index 000000000..436a66d07 --- /dev/null +++ b/src/lib/api/feed/home.ts @@ -0,0 +1,89 @@ +import {AppBskyFeedDefs} from '@atproto/api' +import {FeedAPI, FeedAPIResponse} from './types' +import {FollowingFeedAPI} from './following' +import {CustomFeedAPI} from './custom' +import {PROD_DEFAULT_FEED} from '#/lib/constants' + +// HACK +// the feed API does not include any facilities for passing down +// non-post elements. adding that is a bit of a heavy lift, and we +// have just one temporary usecase for it: flagging when the home feed +// falls back to discover. +// we use this fallback marker post to drive this instead. see Feed.tsx +// for the usage. +// -prf +export const FALLBACK_MARKER_POST: AppBskyFeedDefs.FeedViewPost = { + post: { + uri: 'fallback-marker-post', + cid: 'fake', + record: {}, + author: { + did: 'did:fake', + handle: 'fake.com', + }, + indexedAt: new Date().toISOString(), + }, +} + +export class HomeFeedAPI implements FeedAPI { + following: FollowingFeedAPI + discover: CustomFeedAPI + usingDiscover = false + itemCursor = 0 + + constructor() { + this.following = new FollowingFeedAPI() + this.discover = new CustomFeedAPI({feed: PROD_DEFAULT_FEED('whats-hot')}) + } + + reset() { + this.following = new FollowingFeedAPI() + this.discover = new CustomFeedAPI({feed: PROD_DEFAULT_FEED('whats-hot')}) + this.usingDiscover = false + this.itemCursor = 0 + } + + async peekLatest(): Promise<AppBskyFeedDefs.FeedViewPost> { + if (this.usingDiscover) { + return this.discover.peekLatest() + } + return this.following.peekLatest() + } + + async fetch({ + cursor, + limit, + }: { + cursor: string | undefined + limit: number + }): Promise<FeedAPIResponse> { + if (!cursor) { + this.reset() + } + + let returnCursor + let posts: AppBskyFeedDefs.FeedViewPost[] = [] + + if (!this.usingDiscover) { + const res = await this.following.fetch({cursor, limit}) + returnCursor = res.cursor + posts = posts.concat(res.feed) + if (!returnCursor) { + cursor = '' + posts.push(FALLBACK_MARKER_POST) + this.usingDiscover = true + } + } + + if (this.usingDiscover) { + const res = await this.discover.fetch({cursor, limit}) + returnCursor = res.cursor + posts = posts.concat(res.feed) + } + + return { + cursor: returnCursor, + feed: posts, + } + } +} diff --git a/src/lib/api/feed/merge.ts b/src/lib/api/feed/merge.ts index 2314e2b95..28bf143cb 100644 --- a/src/lib/api/feed/merge.ts +++ b/src/lib/api/feed/merge.ts @@ -8,6 +8,7 @@ import {FeedAPI, FeedAPIResponse, ReasonFeedSource} from './types' import {FeedParams} from '#/state/queries/post-feed' import {FeedTunerFn} from '../feed-manip' import {getAgent} from '#/state/session' +import {getContentLanguages} from '#/state/preferences/languages' const REQUEST_WAIT_MS = 500 // 500ms const POST_AGE_CUTOFF = 60e3 * 60 * 24 // 24hours @@ -25,7 +26,7 @@ export class MergeFeedAPI implements FeedAPI { reset() { this.following = new MergeFeedSource_Following(this.feedTuners) - this.customFeeds = [] // just empty the array, they will be captured in _fetchNext() + this.customFeeds = [] this.feedCursor = 0 this.itemCursor = 0 this.sampleCursor = 0 @@ -231,11 +232,15 @@ class MergeFeedSource_Custom extends MergeFeedSource { limit: number, ): Promise<AppBskyFeedGetTimeline.Response> { try { - const res = await getAgent().app.bsky.feed.getFeed({ - cursor, - limit, - feed: this.feedUri, - }) + const contentLangs = getContentLanguages().join(',') + const res = await getAgent().app.bsky.feed.getFeed( + { + cursor, + limit, + feed: this.feedUri, + }, + {headers: {'Accept-Language': contentLangs}}, + ) // NOTE // some custom feeds fail to enforce the pagination limit // so we manually truncate here diff --git a/src/lib/batchedUpdates.web.ts b/src/lib/batchedUpdates.web.ts index 03147ed67..ba82549b5 100644 --- a/src/lib/batchedUpdates.web.ts +++ b/src/lib/batchedUpdates.web.ts @@ -1,2 +1 @@ -// @ts-ignore export {unstable_batchedUpdates as batchedUpdates} from 'react-dom' diff --git a/src/lib/country-codes.ts b/src/lib/country-codes.ts new file mode 100644 index 000000000..ae0152876 --- /dev/null +++ b/src/lib/country-codes.ts @@ -0,0 +1,256 @@ +import {CountryCode} from 'libphonenumber-js' + +// ISO 3166-1 alpha-2 codes + +export interface CountryCodeMap { + code2: CountryCode + name: string +} + +export const COUNTRY_CODES: CountryCodeMap[] = [ + {code2: 'AF', name: 'Afghanistan (+93)'}, + {code2: 'AX', name: 'Åland Islands (+358)'}, + {code2: 'AL', name: 'Albania (+355)'}, + {code2: 'DZ', name: 'Algeria (+213)'}, + {code2: 'AS', name: 'American Samoa (+1)'}, + {code2: 'AD', name: 'Andorra (+376)'}, + {code2: 'AO', name: 'Angola (+244)'}, + {code2: 'AI', name: 'Anguilla (+1)'}, + {code2: 'AG', name: 'Antigua and Barbuda (+1)'}, + {code2: 'AR', name: 'Argentina (+54)'}, + {code2: 'AM', name: 'Armenia (+374)'}, + {code2: 'AW', name: 'Aruba (+297)'}, + {code2: 'AU', name: 'Australia (+61)'}, + {code2: 'AT', name: 'Austria (+43)'}, + {code2: 'AZ', name: 'Azerbaijan (+994)'}, + {code2: 'BS', name: 'Bahamas (+1)'}, + {code2: 'BH', name: 'Bahrain (+973)'}, + {code2: 'BD', name: 'Bangladesh (+880)'}, + {code2: 'BB', name: 'Barbados (+1)'}, + {code2: 'BY', name: 'Belarus (+375)'}, + {code2: 'BE', name: 'Belgium (+32)'}, + {code2: 'BZ', name: 'Belize (+501)'}, + {code2: 'BJ', name: 'Benin (+229)'}, + {code2: 'BM', name: 'Bermuda (+1)'}, + {code2: 'BT', name: 'Bhutan (+975)'}, + {code2: 'BO', name: 'Bolivia (Plurinational State of) (+591)'}, + {code2: 'BQ', name: 'Bonaire, Sint Eustatius and Saba (+599)'}, + {code2: 'BA', name: 'Bosnia and Herzegovina (+387)'}, + {code2: 'BW', name: 'Botswana (+267)'}, + {code2: 'BR', name: 'Brazil (+55)'}, + {code2: 'IO', name: 'British Indian Ocean Territory (+246)'}, + {code2: 'BN', name: 'Brunei Darussalam (+673)'}, + {code2: 'BG', name: 'Bulgaria (+359)'}, + {code2: 'BF', name: 'Burkina Faso (+226)'}, + {code2: 'BI', name: 'Burundi (+257)'}, + {code2: 'CV', name: 'Cabo Verde (+238)'}, + {code2: 'KH', name: 'Cambodia (+855)'}, + {code2: 'CM', name: 'Cameroon (+237)'}, + {code2: 'CA', name: 'Canada (+1)'}, + {code2: 'KY', name: 'Cayman Islands (+1)'}, + {code2: 'CF', name: 'Central African Republic (+236)'}, + {code2: 'TD', name: 'Chad (+235)'}, + {code2: 'CL', name: 'Chile (+56)'}, + {code2: 'CN', name: 'China (+86)'}, + {code2: 'CX', name: 'Christmas Island (+61)'}, + {code2: 'CC', name: 'Cocos (Keeling) Islands (+61)'}, + {code2: 'CO', name: 'Colombia (+57)'}, + {code2: 'KM', name: 'Comoros (+269)'}, + {code2: 'CG', name: 'Congo (+242)'}, + {code2: 'CD', name: 'Congo, Democratic Republic of the (+243)'}, + {code2: 'CK', name: 'Cook Islands (+682)'}, + {code2: 'CR', name: 'Costa Rica (+506)'}, + {code2: 'CI', name: "Côte d'Ivoire (+225)"}, + {code2: 'HR', name: 'Croatia (+385)'}, + {code2: 'CU', name: 'Cuba (+53)'}, + {code2: 'CW', name: 'Curaçao (+599)'}, + {code2: 'CY', name: 'Cyprus (+357)'}, + {code2: 'CZ', name: 'Czechia (+420)'}, + {code2: 'DK', name: 'Denmark (+45)'}, + {code2: 'DJ', name: 'Djibouti (+253)'}, + {code2: 'DM', name: 'Dominica (+1)'}, + {code2: 'DO', name: 'Dominican Republic (+1)'}, + {code2: 'EC', name: 'Ecuador (+593)'}, + {code2: 'EG', name: 'Egypt (+20)'}, + {code2: 'SV', name: 'El Salvador (+503)'}, + {code2: 'GQ', name: 'Equatorial Guinea (+240)'}, + {code2: 'ER', name: 'Eritrea (+291)'}, + {code2: 'EE', name: 'Estonia (+372)'}, + {code2: 'SZ', name: 'Eswatini (+268)'}, + {code2: 'ET', name: 'Ethiopia (+251)'}, + {code2: 'FK', name: 'Falkland Islands (Malvinas) (+500)'}, + {code2: 'FO', name: 'Faroe Islands (+298)'}, + {code2: 'FJ', name: 'Fiji (+679)'}, + {code2: 'FI', name: 'Finland (+358)'}, + {code2: 'FR', name: 'France (+33)'}, + {code2: 'GF', name: 'French Guiana (+594)'}, + {code2: 'PF', name: 'French Polynesia (+689)'}, + {code2: 'GA', name: 'Gabon (+241)'}, + {code2: 'GM', name: 'Gambia (+220)'}, + {code2: 'GE', name: 'Georgia (+995)'}, + {code2: 'DE', name: 'Germany (+49)'}, + {code2: 'GH', name: 'Ghana (+233)'}, + {code2: 'GI', name: 'Gibraltar (+350)'}, + {code2: 'GR', name: 'Greece (+30)'}, + {code2: 'GL', name: 'Greenland (+299)'}, + {code2: 'GD', name: 'Grenada (+1)'}, + {code2: 'GP', name: 'Guadeloupe (+590)'}, + {code2: 'GU', name: 'Guam (+1)'}, + {code2: 'GT', name: 'Guatemala (+502)'}, + {code2: 'GG', name: 'Guernsey (+44)'}, + {code2: 'GN', name: 'Guinea (+224)'}, + {code2: 'GW', name: 'Guinea-Bissau (+245)'}, + {code2: 'GY', name: 'Guyana (+592)'}, + {code2: 'HT', name: 'Haiti (+509)'}, + {code2: 'VA', name: 'Holy See (+39)'}, + {code2: 'HN', name: 'Honduras (+504)'}, + {code2: 'HK', name: 'Hong Kong (+852)'}, + {code2: 'HU', name: 'Hungary (+36)'}, + {code2: 'IS', name: 'Iceland (+354)'}, + {code2: 'IN', name: 'India (+91)'}, + {code2: 'ID', name: 'Indonesia (+62)'}, + {code2: 'IR', name: 'Iran (Islamic Republic of) (+98)'}, + {code2: 'IQ', name: 'Iraq (+964)'}, + {code2: 'IE', name: 'Ireland (+353)'}, + {code2: 'IM', name: 'Isle of Man (+44)'}, + {code2: 'IL', name: 'Israel (+972)'}, + {code2: 'IT', name: 'Italy (+39)'}, + {code2: 'JM', name: 'Jamaica (+1)'}, + {code2: 'JP', name: 'Japan (+81)'}, + {code2: 'JE', name: 'Jersey (+44)'}, + {code2: 'JO', name: 'Jordan (+962)'}, + {code2: 'KZ', name: 'Kazakhstan (+7)'}, + {code2: 'KE', name: 'Kenya (+254)'}, + {code2: 'KI', name: 'Kiribati (+686)'}, + {code2: 'KP', name: "Korea (Democratic People's Republic of) (+850)"}, + {code2: 'KR', name: 'Korea, Republic of (+82)'}, + {code2: 'KW', name: 'Kuwait (+965)'}, + {code2: 'KG', name: 'Kyrgyzstan (+996)'}, + {code2: 'LA', name: "Lao People's Democratic Republic (+856)"}, + {code2: 'LV', name: 'Latvia (+371)'}, + {code2: 'LB', name: 'Lebanon (+961)'}, + {code2: 'LS', name: 'Lesotho (+266)'}, + {code2: 'LR', name: 'Liberia (+231)'}, + {code2: 'LY', name: 'Libya (+218)'}, + {code2: 'LI', name: 'Liechtenstein (+423)'}, + {code2: 'LT', name: 'Lithuania (+370)'}, + {code2: 'LU', name: 'Luxembourg (+352)'}, + {code2: 'MO', name: 'Macao (+853)'}, + {code2: 'MG', name: 'Madagascar (+261)'}, + {code2: 'MW', name: 'Malawi (+265)'}, + {code2: 'MY', name: 'Malaysia (+60)'}, + {code2: 'MV', name: 'Maldives (+960)'}, + {code2: 'ML', name: 'Mali (+223)'}, + {code2: 'MT', name: 'Malta (+356)'}, + {code2: 'MH', name: 'Marshall Islands (+692)'}, + {code2: 'MQ', name: 'Martinique (+596)'}, + {code2: 'MR', name: 'Mauritania (+222)'}, + {code2: 'MU', name: 'Mauritius (+230)'}, + {code2: 'YT', name: 'Mayotte (+262)'}, + {code2: 'MX', name: 'Mexico (+52)'}, + {code2: 'FM', name: 'Micronesia (Federated States of) (+691)'}, + {code2: 'MD', name: 'Moldova, Republic of (+373)'}, + {code2: 'MC', name: 'Monaco (+377)'}, + {code2: 'MN', name: 'Mongolia (+976)'}, + {code2: 'ME', name: 'Montenegro (+382)'}, + {code2: 'MS', name: 'Montserrat (+1)'}, + {code2: 'MA', name: 'Morocco (+212)'}, + {code2: 'MZ', name: 'Mozambique (+258)'}, + {code2: 'MM', name: 'Myanmar (+95)'}, + {code2: 'NA', name: 'Namibia (+264)'}, + {code2: 'NR', name: 'Nauru (+674)'}, + {code2: 'NP', name: 'Nepal (+977)'}, + {code2: 'NL', name: 'Netherlands, Kingdom of the (+31)'}, + {code2: 'NC', name: 'New Caledonia (+687)'}, + {code2: 'NZ', name: 'New Zealand (+64)'}, + {code2: 'NI', name: 'Nicaragua (+505)'}, + {code2: 'NE', name: 'Niger (+227)'}, + {code2: 'NG', name: 'Nigeria (+234)'}, + {code2: 'NU', name: 'Niue (+683)'}, + {code2: 'NF', name: 'Norfolk Island (+672)'}, + {code2: 'MK', name: 'North Macedonia (+389)'}, + {code2: 'MP', name: 'Northern Mariana Islands (+1)'}, + {code2: 'NO', name: 'Norway (+47)'}, + {code2: 'OM', name: 'Oman (+968)'}, + {code2: 'PK', name: 'Pakistan (+92)'}, + {code2: 'PW', name: 'Palau (+680)'}, + {code2: 'PS', name: 'Palestine, State of (+970)'}, + {code2: 'PA', name: 'Panama (+507)'}, + {code2: 'PG', name: 'Papua New Guinea (+675)'}, + {code2: 'PY', name: 'Paraguay (+595)'}, + {code2: 'PE', name: 'Peru (+51)'}, + {code2: 'PH', name: 'Philippines (+63)'}, + {code2: 'PL', name: 'Poland (+48)'}, + {code2: 'PT', name: 'Portugal (+351)'}, + {code2: 'PR', name: 'Puerto Rico (+1)'}, + {code2: 'QA', name: 'Qatar (+974)'}, + {code2: 'RE', name: 'Réunion (+262)'}, + {code2: 'RO', name: 'Romania (+40)'}, + {code2: 'RU', name: 'Russian Federation (+7)'}, + {code2: 'RW', name: 'Rwanda (+250)'}, + {code2: 'BL', name: 'Saint Barthélemy (+590)'}, + {code2: 'SH', name: 'Saint Helena, Ascension and Tristan da Cunha (+290)'}, + {code2: 'KN', name: 'Saint Kitts and Nevis (+1)'}, + {code2: 'LC', name: 'Saint Lucia (+1)'}, + {code2: 'MF', name: 'Saint Martin (French part) (+590)'}, + {code2: 'PM', name: 'Saint Pierre and Miquelon (+508)'}, + {code2: 'VC', name: 'Saint Vincent and the Grenadines (+1)'}, + {code2: 'WS', name: 'Samoa (+685)'}, + {code2: 'SM', name: 'San Marino (+378)'}, + {code2: 'ST', name: 'Sao Tome and Principe (+239)'}, + {code2: 'SA', name: 'Saudi Arabia (+966)'}, + {code2: 'SN', name: 'Senegal (+221)'}, + {code2: 'RS', name: 'Serbia (+381)'}, + {code2: 'SC', name: 'Seychelles (+248)'}, + {code2: 'SL', name: 'Sierra Leone (+232)'}, + {code2: 'SG', name: 'Singapore (+65)'}, + {code2: 'SX', name: 'Sint Maarten (Dutch part) (+1)'}, + {code2: 'SK', name: 'Slovakia (+421)'}, + {code2: 'SI', name: 'Slovenia (+386)'}, + {code2: 'SB', name: 'Solomon Islands (+677)'}, + {code2: 'SO', name: 'Somalia (+252)'}, + {code2: 'ZA', name: 'South Africa (+27)'}, + {code2: 'SS', name: 'South Sudan (+211)'}, + {code2: 'ES', name: 'Spain (+34)'}, + {code2: 'LK', name: 'Sri Lanka (+94)'}, + {code2: 'SD', name: 'Sudan (+249)'}, + {code2: 'SR', name: 'Suriname (+597)'}, + {code2: 'SJ', name: 'Svalbard and Jan Mayen (+47)'}, + {code2: 'SE', name: 'Sweden (+46)'}, + {code2: 'CH', name: 'Switzerland (+41)'}, + {code2: 'SY', name: 'Syrian Arab Republic (+963)'}, + {code2: 'TW', name: 'Taiwan, Province of China (+886)'}, + {code2: 'TJ', name: 'Tajikistan (+992)'}, + {code2: 'TZ', name: 'Tanzania, United Republic of (+255)'}, + {code2: 'TH', name: 'Thailand (+66)'}, + {code2: 'TL', name: 'Timor-Leste (+670)'}, + {code2: 'TG', name: 'Togo (+228)'}, + {code2: 'TK', name: 'Tokelau (+690)'}, + {code2: 'TO', name: 'Tonga (+676)'}, + {code2: 'TT', name: 'Trinidad and Tobago (+1)'}, + {code2: 'TN', name: 'Tunisia (+216)'}, + {code2: 'TR', name: 'Türkiye (+90)'}, + {code2: 'TM', name: 'Turkmenistan (+993)'}, + {code2: 'TC', name: 'Turks and Caicos Islands (+1)'}, + {code2: 'TV', name: 'Tuvalu (+688)'}, + {code2: 'UG', name: 'Uganda (+256)'}, + {code2: 'UA', name: 'Ukraine (+380)'}, + {code2: 'AE', name: 'United Arab Emirates (+971)'}, + { + code2: 'GB', + name: 'United Kingdom of Great Britain and Northern Ireland (+44)', + }, + {code2: 'US', name: 'United States of America (+1)'}, + {code2: 'UY', name: 'Uruguay (+598)'}, + {code2: 'UZ', name: 'Uzbekistan (+998)'}, + {code2: 'VU', name: 'Vanuatu (+678)'}, + {code2: 'VE', name: 'Venezuela (Bolivarian Republic of) (+58)'}, + {code2: 'VN', name: 'Viet Nam (+84)'}, + {code2: 'VG', name: 'Virgin Islands (British) (+1)'}, + {code2: 'VI', name: 'Virgin Islands (U.S.) (+1)'}, + {code2: 'WF', name: 'Wallis and Futuna (+681)'}, + {code2: 'EH', name: 'Western Sahara (+212)'}, + {code2: 'YE', name: 'Yemen (+967)'}, + {code2: 'ZM', name: 'Zambia (+260)'}, + {code2: 'ZW', name: 'Zimbabwe (+263)'}, +] diff --git a/src/lib/hooks/useWebBodyScrollLock.ts b/src/lib/hooks/useWebBodyScrollLock.ts new file mode 100644 index 000000000..585f193f1 --- /dev/null +++ b/src/lib/hooks/useWebBodyScrollLock.ts @@ -0,0 +1,28 @@ +import {useEffect} from 'react' +import {isWeb} from '#/platform/detection' + +let refCount = 0 + +function incrementRefCount() { + if (refCount === 0) { + document.body.style.overflow = 'hidden' + } + refCount++ +} + +function decrementRefCount() { + refCount-- + if (refCount === 0) { + document.body.style.overflow = '' + } +} + +export function useWebBodyScrollLock(isLockActive: boolean) { + useEffect(() => { + if (!isWeb || !isLockActive) { + return + } + incrementRefCount() + return () => decrementRefCount() + }) +} diff --git a/src/lib/hooks/useWebScrollRestoration.native.ts b/src/lib/hooks/useWebScrollRestoration.native.ts new file mode 100644 index 000000000..c7d96607f --- /dev/null +++ b/src/lib/hooks/useWebScrollRestoration.native.ts @@ -0,0 +1,3 @@ +export function useWebScrollRestoration() { + return undefined +} diff --git a/src/lib/hooks/useWebScrollRestoration.ts b/src/lib/hooks/useWebScrollRestoration.ts new file mode 100644 index 000000000..f68fbf0f2 --- /dev/null +++ b/src/lib/hooks/useWebScrollRestoration.ts @@ -0,0 +1,52 @@ +import {useMemo, useState, useEffect} from 'react' +import {EventArg, useNavigation} from '@react-navigation/core' + +if ('scrollRestoration' in history) { + // Tell the brower not to mess with the scroll. + // We're doing that manually below. + history.scrollRestoration = 'manual' +} + +function createInitialScrollState() { + return { + scrollYs: new Map(), + focusedKey: null as string | null, + } +} + +export function useWebScrollRestoration() { + const [state] = useState(createInitialScrollState) + const navigation = useNavigation() + + useEffect(() => { + function onDispatch() { + if (state.focusedKey) { + // Remember where we were for later. + state.scrollYs.set(state.focusedKey, window.scrollY) + // TODO: Strictly speaking, this is a leak. We never clean up. + // This is because I'm not sure when it's appropriate to clean it up. + // It doesn't seem like popstate is enough because it can still Forward-Back again. + // Maybe we should use sessionStorage. Or check what Next.js is doing? + } + } + // We want to intercept any push/pop/replace *before* the re-render. + // There is no official way to do this yet, but this works okay for now. + // https://twitter.com/satya164/status/1737301243519725803 + navigation.addListener('__unsafe_action__' as any, onDispatch) + return () => { + navigation.removeListener('__unsafe_action__' as any, onDispatch) + } + }, [state, navigation]) + + const screenListeners = useMemo( + () => ({ + focus(e: EventArg<'focus', boolean | undefined, unknown>) { + const scrollY = state.scrollYs.get(e.target) ?? 0 + window.scrollTo(0, scrollY) + state.focusedKey = e.target ?? null + }, + }), + [state], + ) + return screenListeners +} diff --git a/src/lib/link-meta/bsky.ts b/src/lib/link-meta/bsky.ts index 322b02332..c1fbb34b3 100644 --- a/src/lib/link-meta/bsky.ts +++ b/src/lib/link-meta/bsky.ts @@ -5,6 +5,7 @@ import {LikelyType, LinkMeta} from './link-meta' import {convertBskyAppUrlIfNeeded, makeRecordUri} from '../strings/url-helpers' import {ComposerOptsQuote} from 'state/shell/composer' import {useGetPost} from '#/state/queries/post' +import {useFetchDid} from '#/state/queries/handle' // TODO // import {Home} from 'view/screens/Home' @@ -120,11 +121,13 @@ export async function getPostAsQuote( export async function getFeedAsEmbed( agent: BskyAgent, + fetchDid: ReturnType<typeof useFetchDid>, url: string, ): Promise<apilib.ExternalEmbedDraft> { url = convertBskyAppUrlIfNeeded(url) - const [_0, user, _1, rkey] = url.split('/').filter(Boolean) - const feed = makeRecordUri(user, 'app.bsky.feed.generator', rkey) + const [_0, handleOrDid, _1, rkey] = url.split('/').filter(Boolean) + const did = await fetchDid(handleOrDid) + const feed = makeRecordUri(did, 'app.bsky.feed.generator', rkey) const res = await agent.app.bsky.feed.getFeedGenerator({feed}) return { isLoading: false, @@ -146,11 +149,13 @@ export async function getFeedAsEmbed( export async function getListAsEmbed( agent: BskyAgent, + fetchDid: ReturnType<typeof useFetchDid>, url: string, ): Promise<apilib.ExternalEmbedDraft> { url = convertBskyAppUrlIfNeeded(url) - const [_0, user, _1, rkey] = url.split('/').filter(Boolean) - const list = makeRecordUri(user, 'app.bsky.graph.list', rkey) + const [_0, handleOrDid, _1, rkey] = url.split('/').filter(Boolean) + const did = await fetchDid(handleOrDid) + const list = makeRecordUri(did, 'app.bsky.graph.list', rkey) const res = await agent.app.bsky.graph.getList({list}) return { isLoading: false, diff --git a/src/lib/strings/embed-player.ts b/src/lib/strings/embed-player.ts index 0f97eb080..3270b6f07 100644 --- a/src/lib/strings/embed-player.ts +++ b/src/lib/strings/embed-player.ts @@ -68,11 +68,12 @@ export function parseEmbedPlayerFromUrl( // youtube if (urlp.hostname === 'youtu.be') { const videoId = urlp.pathname.split('/')[1] + const seek = encodeURIComponent(urlp.searchParams.get('t') ?? 0) if (videoId) { return { type: 'youtube_video', source: 'youtube', - playerUri: `https://www.youtube.com/embed/${videoId}?autoplay=1&playsinline=1`, + playerUri: `https://www.youtube.com/embed/${videoId}?autoplay=1&playsinline=1&start=${seek}`, } } } @@ -84,13 +85,14 @@ export function parseEmbedPlayerFromUrl( const [_, page, shortVideoId] = urlp.pathname.split('/') const videoId = page === 'shorts' ? shortVideoId : (urlp.searchParams.get('v') as string) + const seek = encodeURIComponent(urlp.searchParams.get('t') ?? 0) if (videoId) { return { type: page === 'shorts' ? 'youtube_short' : 'youtube_video', source: page === 'shorts' ? 'youtubeShorts' : 'youtube', hideDetails: page === 'shorts' ? true : undefined, - playerUri: `https://www.youtube.com/embed/${videoId}?autoplay=1&playsinline=1`, + playerUri: `https://www.youtube.com/embed/${videoId}?autoplay=1&playsinline=1&start=${seek}`, } } } diff --git a/src/lib/strings/rich-text-helpers.ts b/src/lib/strings/rich-text-helpers.ts index 08971ca03..662004599 100644 --- a/src/lib/strings/rich-text-helpers.ts +++ b/src/lib/strings/rich-text-helpers.ts @@ -1,7 +1,7 @@ import {AppBskyRichtextFacet, RichText} from '@atproto/api' import {linkRequiresWarning} from './url-helpers' -export function richTextToString(rt: RichText): string { +export function richTextToString(rt: RichText, loose: boolean): string { const {text, facets} = rt if (!facets?.length) { @@ -19,7 +19,7 @@ export function richTextToString(rt: RichText): string { const requiresWarning = linkRequiresWarning(href, text) - result += !requiresWarning ? href : `[${text}](${href})` + result += !requiresWarning ? href : loose ? `[${text}](${href})` : text } else { result += segment.text } diff --git a/src/lib/styles.ts b/src/lib/styles.ts index 5a10fea86..df9b49260 100644 --- a/src/lib/styles.ts +++ b/src/lib/styles.ts @@ -1,6 +1,6 @@ import {Dimensions, StyleProp, StyleSheet, TextStyle} from 'react-native' import {Theme, TypographyVariant} from './ThemeContext' -import {isMobileWeb} from 'platform/detection' +import {isWeb} from 'platform/detection' // 1 is lightest, 2 is light, 3 is mid, 4 is dark, 5 is darkest export const colors = { @@ -175,7 +175,7 @@ export const s = StyleSheet.create({ // dimensions w100pct: {width: '100%'}, h100pct: {height: '100%'}, - hContentRegion: isMobileWeb ? {flex: 1} : {height: '100%'}, + hContentRegion: isWeb ? {minHeight: '100%'} : {height: '100%'}, window: { width: Dimensions.get('window').width, height: Dimensions.get('window').height, diff --git a/src/lib/themes.ts b/src/lib/themes.ts index ad7574db6..2d4515c77 100644 --- a/src/lib/themes.ts +++ b/src/lib/themes.ts @@ -2,30 +2,32 @@ import {Platform} from 'react-native' import type {Theme} from './ThemeContext' import {colors} from './styles' +import {darkPalette, lightPalette} from '#/alf/themes' + export const defaultTheme: Theme = { colorScheme: 'light', palette: { default: { - background: colors.white, - backgroundLight: colors.gray1, - text: colors.black, - textLight: colors.gray5, - textInverted: colors.white, - link: colors.blue3, - border: '#f0e9e9', - borderDark: '#e0d9d9', - icon: colors.gray4, + background: lightPalette.white, + backgroundLight: lightPalette.contrast_50, + text: lightPalette.black, + textLight: lightPalette.contrast_700, + textInverted: lightPalette.white, + link: lightPalette.primary_500, + border: lightPalette.contrast_100, + borderDark: lightPalette.contrast_200, + icon: lightPalette.contrast_500, // non-standard - textVeryLight: colors.gray4, - replyLine: colors.gray2, - replyLineDot: colors.gray3, - unreadNotifBg: '#ebf6ff', - unreadNotifBorder: colors.blue1, - postCtrl: '#71768A', - brandText: '#0066FF', - emptyStateIcon: '#B6B6C9', - borderLinkHover: '#cac1c1', + textVeryLight: lightPalette.contrast_400, + replyLine: lightPalette.contrast_100, + replyLineDot: lightPalette.contrast_200, + unreadNotifBg: lightPalette.primary_25, + unreadNotifBorder: lightPalette.primary_100, + postCtrl: lightPalette.contrast_500, + brandText: lightPalette.primary_500, + emptyStateIcon: lightPalette.contrast_300, + borderLinkHover: lightPalette.contrast_300, }, primary: { background: colors.blue3, @@ -50,15 +52,15 @@ export const defaultTheme: Theme = { icon: colors.green4, }, inverted: { - background: colors.black, - backgroundLight: colors.gray6, - text: colors.white, - textLight: colors.gray3, - textInverted: colors.black, - link: colors.blue2, - border: colors.gray3, - borderDark: colors.gray2, - icon: colors.gray5, + background: darkPalette.black, + backgroundLight: darkPalette.contrast_50, + text: darkPalette.white, + textLight: darkPalette.contrast_700, + textInverted: darkPalette.black, + link: darkPalette.primary_500, + border: darkPalette.contrast_100, + borderDark: darkPalette.contrast_200, + icon: darkPalette.contrast_500, }, error: { background: colors.red3, @@ -292,26 +294,26 @@ export const darkTheme: Theme = { palette: { ...defaultTheme.palette, default: { - background: colors.black, - backgroundLight: colors.gray7, - text: colors.white, - textLight: colors.gray3, - textInverted: colors.black, - link: colors.blue3, - border: colors.gray7, - borderDark: colors.gray6, - icon: colors.gray4, + background: darkPalette.black, + backgroundLight: darkPalette.contrast_50, + text: darkPalette.white, + textLight: darkPalette.contrast_700, + textInverted: darkPalette.black, + link: darkPalette.primary_500, + border: darkPalette.contrast_100, + borderDark: darkPalette.contrast_200, + icon: darkPalette.contrast_500, // non-standard - textVeryLight: colors.gray4, - replyLine: colors.gray5, - replyLineDot: colors.gray6, - unreadNotifBg: colors.blue7, - unreadNotifBorder: colors.blue6, - postCtrl: '#707489', - brandText: '#0085ff', - emptyStateIcon: colors.gray4, - borderLinkHover: colors.gray5, + textVeryLight: darkPalette.contrast_400, + replyLine: darkPalette.contrast_100, + replyLineDot: darkPalette.contrast_200, + unreadNotifBg: darkPalette.primary_975, + unreadNotifBorder: darkPalette.primary_900, + postCtrl: darkPalette.contrast_500, + brandText: darkPalette.primary_500, + emptyStateIcon: darkPalette.contrast_300, + borderLinkHover: darkPalette.contrast_300, }, primary: { ...defaultTheme.palette.primary, @@ -322,15 +324,15 @@ export const darkTheme: Theme = { textInverted: colors.green2, }, inverted: { - background: colors.white, - backgroundLight: colors.gray2, - text: colors.black, - textLight: colors.gray5, - textInverted: colors.white, - link: colors.blue3, - border: colors.gray3, - borderDark: colors.gray4, - icon: colors.gray1, + background: lightPalette.white, + backgroundLight: lightPalette.contrast_50, + text: lightPalette.black, + textLight: lightPalette.contrast_700, + textInverted: lightPalette.white, + link: lightPalette.primary_500, + border: lightPalette.contrast_100, + borderDark: lightPalette.contrast_200, + icon: lightPalette.contrast_500, }, }, } |