diff --git a/.eslintrc.js b/.eslintrc.js index 2d59d36d..19fcf230 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1,6 +1,10 @@ module.exports = { root: true, - extends: ['@react-native-community', 'plugin:react-native-a11y/ios'], + extends: [ + '@react-native-community', + 'plugin:react-native-a11y/ios', + 'prettier', + ], parser: '@typescript-eslint/parser', plugins: ['@typescript-eslint', 'detox'], ignorePatterns: [ diff --git a/Dockerfile b/Dockerfile index fbd13beb..241926db 100644 --- a/Dockerfile +++ b/Dockerfile @@ -17,7 +17,7 @@ ENV CGO_ENABLED=1 COPY . . # -# Generate the Javascript webpack. +# Generate the JavaScript webpack. # RUN mkdir --parents $NVM_DIR && \ wget \ @@ -36,7 +36,7 @@ RUN \. "$NVM_DIR/nvm.sh" && \ RUN find ./bskyweb/static && find ./web-build/static # -# Generate the bksyweb Go binary. +# Generate the bskyweb Go binary. # RUN cd bskyweb/ && \ go mod download && \ diff --git a/README.md b/README.md index 1782b8ee..ead1e677 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ Welcome friends! This is the codebase for the Bluesky Social app. It serves as a resource to engineers building on the [AT Protocol](https://atproto.com). -- **Web: [staging.bsky.app](https://staging.bsky.app)** +- **Web: [bsky.app](https://bsky.app)** - **iOS: [App Store](https://apps.apple.com/us/app/bluesky-social/id6444370199)** - **Android: [Play Store](https://play.google.com/store/apps/details?id=xyz.blueskyweb.app&hl=en_US&gl=US)** diff --git a/__tests__/lib/link-meta.test.ts b/__tests__/lib/link-meta.test.ts index f0ca7a9d..504b11c2 100644 --- a/__tests__/lib/link-meta.test.ts +++ b/__tests__/lib/link-meta.test.ts @@ -1,106 +1,4 @@ -import { - LikelyType, - getLinkMeta, - getLikelyType, -} from '../../src/lib/link-meta/link-meta' -import {exampleComHtml} from './__mocks__/exampleComHtml' -import {BskyAgent} from '@atproto/api' -import {DEFAULT_SERVICE, RootStoreModel} from '../../src/state' - -describe('getLinkMeta', () => { - let rootStore: RootStoreModel - - beforeEach(() => { - rootStore = new RootStoreModel(new BskyAgent({service: DEFAULT_SERVICE})) - }) - - const inputs = [ - '', - 'httpbadurl', - 'https://example.com', - 'https://example.com/index.html', - 'https://example.com/image.png', - 'https://example.com/video.avi', - 'https://example.com/audio.ogg', - 'https://example.com/text.txt', - 'https://example.com/javascript.js', - 'https://bsky.app/', - 'https://bsky.app/index.html', - ] - const outputs = [ - { - error: 'Invalid URL', - likelyType: LikelyType.Other, - url: '', - }, - { - error: 'Invalid URL', - likelyType: LikelyType.Other, - url: 'httpbadurl', - }, - { - likelyType: LikelyType.HTML, - url: 'https://example.com', - title: 'Example Domain', - description: 'An example website', - }, - { - likelyType: LikelyType.HTML, - url: 'https://example.com/index.html', - title: 'Example Domain', - description: 'An example website', - }, - { - likelyType: LikelyType.Image, - url: 'https://example.com/image.png', - }, - { - likelyType: LikelyType.Video, - url: 'https://example.com/video.avi', - }, - { - likelyType: LikelyType.Audio, - url: 'https://example.com/audio.ogg', - }, - { - likelyType: LikelyType.Text, - url: 'https://example.com/text.txt', - }, - { - likelyType: LikelyType.Other, - url: 'https://example.com/javascript.js', - }, - { - likelyType: LikelyType.AtpData, - url: '/', - }, - { - likelyType: LikelyType.AtpData, - url: '/index.html', - }, - { - likelyType: LikelyType.Other, - url: '', - title: '', - }, - ] - it('correctly handles a set of text inputs', async () => { - for (let i = 0; i < inputs.length; i++) { - global.fetch = jest.fn().mockImplementationOnce(() => { - return new Promise((resolve, _reject) => { - resolve({ - ok: true, - status: 200, - text: () => exampleComHtml, - }) - }) - }) - const input = inputs[i] - const output = await getLinkMeta(rootStore, input) - expect(output).toEqual(outputs[i]) - } - }) -}) +import {LikelyType, getLikelyType} from '../../src/lib/link-meta/link-meta' describe('getLikelyType', () => { it('correctly handles non-parsed url', async () => { diff --git a/__tests__/lib/string.test.ts b/__tests__/lib/string.test.ts index f25bd02a..75d7b842 100644 --- a/__tests__/lib/string.test.ts +++ b/__tests__/lib/string.test.ts @@ -48,7 +48,7 @@ describe('detectLinkables', () => { 'Classic article https://socket3.wordpress.com/2018/02/03/designing-windows-95s-user-interface/ ', 'https://foo.com https://bar.com/whatever https://baz.com', 'punctuation https://foo.com, https://bar.com/whatever; https://baz.com.', - 'parenthentical (https://foo.com)', + 'parenthetical (https://foo.com)', 'except for https://foo.com/thing_(cool)', ] const outputs = [ @@ -112,7 +112,7 @@ describe('detectLinkables', () => { {link: 'https://baz.com'}, '.', ], - ['parenthentical (', {link: 'https://foo.com'}, ')'], + ['parenthetical (', {link: 'https://foo.com'}, ')'], ['except for ', {link: 'https://foo.com/thing_(cool)'}], ] it('correctly handles a set of text inputs', () => { @@ -176,16 +176,20 @@ describe('ago', () => { new Date().setMinutes(new Date().getMinutes() - 10), new Date().setHours(new Date().getHours() - 1), new Date().setDate(new Date().getDate() - 1), + new Date().setDate(new Date().getDate() - 6), + new Date().setDate(new Date().getDate() - 7), new Date().setMonth(new Date().getMonth() - 1), ] const outputs = [ - new Date(1671461038).toLocaleDateString(), - new Date('04 Dec 1995 00:12:00 GMT').toLocaleDateString(), + new Date(1671461038).toLocaleDateString('en-us', {year: 'numeric', month: 'short', day: 'numeric'}), + new Date('04 Dec 1995 00:12:00 GMT').toLocaleDateString('en-us', {year: 'numeric', month: 'short', day: 'numeric'}), '0s', '10m', '1h', '1d', - '1mo', + '6d', + new Date(new Date().setDate(new Date().getDate() - 7)).toLocaleDateString('en-us', {year: 'numeric', month: 'short', day: 'numeric'}), + new Date(new Date().setMonth(new Date().getMonth() - 1)).toLocaleDateString('en-us', {year: 'numeric', month: 'short', day: 'numeric'}), ] it('correctly calculates how much time passed, in a string', () => { diff --git a/app.json b/app.json index 8a2fe039..9016a364 100644 --- a/app.json +++ b/app.json @@ -4,7 +4,7 @@ "slug": "bluesky", "scheme": "bluesky", "owner": "blueskysocial", - "version": "1.28.0", + "version": "1.29.0", "orientation": "portrait", "icon": "./assets/icon.png", "userInterfaceStyle": "light", @@ -40,7 +40,7 @@ "backgroundColor": "#ffffff" }, "android": { - "versionCode": 14, + "versionCode": 15, "adaptiveIcon": { "foregroundImage": "./assets/adaptive-icon.png", "backgroundColor": "#ffffff" diff --git a/bskyweb/README.md b/bskyweb/README.md index a74cda0d..d6064737 100644 --- a/bskyweb/README.md +++ b/bskyweb/README.md @@ -2,7 +2,7 @@ ### SPA Bundle (monolithic static javascript file) -To build the SPA bundle (`bundle.web.js`), first get a Javascript development +To build the SPA bundle (`bundle.web.js`), first get a JavaScript development environment set up. Either follow the top-level README, or something quick like: diff --git a/bskyweb/cmd/bskyweb/.gitignore b/bskyweb/cmd/bskyweb/.gitignore new file mode 100644 index 00000000..45883bf1 --- /dev/null +++ b/bskyweb/cmd/bskyweb/.gitignore @@ -0,0 +1 @@ +bskyweb diff --git a/bskyweb/cmd/bskyweb/server.go b/bskyweb/cmd/bskyweb/server.go index 7c230041..cdd4dd1d 100644 --- a/bskyweb/cmd/bskyweb/server.go +++ b/bskyweb/cmd/bskyweb/server.go @@ -2,8 +2,10 @@ package main import ( "context" + "encoding/json" "fmt" "io/fs" + "io/ioutil" "net/http" "os" "strings" @@ -62,6 +64,7 @@ func serve(cctx *cli.Context) error { staticHandler := http.FileServer(func() http.FileSystem { if debug { + log.Debugf("serving static file from the local file system") return http.FS(os.DirFS("static")) } fsys, err := fs.Sub(bskyweb.StaticFS, "static") @@ -98,13 +101,22 @@ func serve(cctx *cli.Context) error { RedirectCode: http.StatusFound, })) + // // configure routes + // + + // static files e.GET("/robots.txt", echo.WrapHandler(staticHandler)) e.GET("/static/*", echo.WrapHandler(http.StripPrefix("/static/", staticHandler))) + e.GET("/.well-known/*", echo.WrapHandler(staticHandler)) + + // home e.GET("/", server.WebHome) // generic routes e.GET("/search", server.WebGeneric) + e.GET("/search/feeds", server.WebGeneric) + e.GET("/feeds", server.WebGeneric) e.GET("/notifications", server.WebGeneric) e.GET("/moderation", server.WebGeneric) e.GET("/moderation/mute-lists", server.WebGeneric) @@ -112,6 +124,7 @@ func serve(cctx *cli.Context) error { e.GET("/moderation/blocked-accounts", server.WebGeneric) e.GET("/settings", server.WebGeneric) e.GET("/settings/app-passwords", server.WebGeneric) + e.GET("/settings/saved-feeds", server.WebGeneric) e.GET("/sys/debug", server.WebGeneric) e.GET("/sys/log", server.WebGeneric) e.GET("/support", server.WebGeneric) @@ -125,6 +138,8 @@ func serve(cctx *cli.Context) error { e.GET("/profile/:handle/follows", server.WebGeneric) e.GET("/profile/:handle/followers", server.WebGeneric) e.GET("/profile/:handle/lists/:rkey", server.WebGeneric) + e.GET("/profile/:handle/feed/:rkey", server.WebGeneric) + e.GET("/profile/:handle/feed/:rkey/liked-by", server.WebGeneric) // post endpoints; only first populates info e.GET("/profile/:handle/post/:rkey", server.WebPost) @@ -132,11 +147,36 @@ func serve(cctx *cli.Context) error { e.GET("/profile/:handle/post/:rkey/reposted-by", server.WebGeneric) // Mailmodo - e.POST("/waitlist", func(c echo.Context) error { - email := strings.TrimSpace(c.FormValue("email")) - if err := mailmodo.AddToList(c.Request().Context(), mailmodoListName, email); err != nil { + e.POST("/api/waitlist", func(c echo.Context) error { + type jsonError struct { + Error string `json:"error"` + } + + // Read the API request. + type apiRequest struct { + Email string `json:"email"` + } + + bodyReader := http.MaxBytesReader(c.Response(), c.Request().Body, 16*1024) + payload, err := ioutil.ReadAll(bodyReader) + if err != nil { return err } + var req apiRequest + if err := json.Unmarshal(payload, &req); err != nil { + return c.JSON(http.StatusBadRequest, jsonError{Error: "Invalid API request"}) + } + + if req.Email == "" { + return c.JSON(http.StatusBadRequest, jsonError{Error: "Please enter a valid email address."}) + } + + if err := mailmodo.AddToList(c.Request().Context(), mailmodoListName, req.Email); err != nil { + log.Errorf("adding email to waitlist failed: %s", err) + return c.JSON(http.StatusBadRequest, jsonError{ + Error: "Storing email in waitlist failed. Please enter a valid email address.", + }) + } return c.JSON(http.StatusOK, map[string]bool{"success": true}) }) diff --git a/bskyweb/static/.well-known/apple-app-site-association b/bskyweb/static/.well-known/apple-app-site-association new file mode 100644 index 00000000..232acdf2 --- /dev/null +++ b/bskyweb/static/.well-known/apple-app-site-association @@ -0,0 +1,13 @@ +{ + "applinks": { + "apps": [], + "details": [ + { + "appID": "B3LX46C5HS.xyz.blueskyweb.app", + "paths": [ + "*" + ] + } + ] + } +} \ No newline at end of file diff --git a/bskyweb/static/.well-known/assetlinks.json b/bskyweb/static/.well-known/assetlinks.json new file mode 100644 index 00000000..5ca12d5b --- /dev/null +++ b/bskyweb/static/.well-known/assetlinks.json @@ -0,0 +1,11 @@ +[ + { + "relation": ["delegate_permission/common.handle_all_urls"], + "target": { + "namespace": "android_app", + "package_name": "xyz.blueskyweb.app", + "sha256_cert_fingerprints": + ["C1:4D:3C:6B:B5:D6:D9:AE:CF:C5:0B:BC:C1:9B:29:6D:D4:E6:87:46:36:D5:4C:1A:64:1C:14:08:BF:7E:F9:62", "FA:C6:17:45:DC:09:03:78:6F:B9:ED:E6:2A:96:2B:39:9F:73:48:F0:BB:6F:89:9B:83:32:66:75:91:03:3B:9C"] + } + } +] diff --git a/bskyweb/templates/base.html b/bskyweb/templates/base.html index 5725b7af..abd312ed 100644 --- a/bskyweb/templates/base.html +++ b/bskyweb/templates/base.html @@ -122,8 +122,8 @@ {%- block body_all %}