From 2b5e437601b007536dbc10f31159294a29aef912 Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Thu, 9 Jul 2026 21:06:40 +0300 Subject: [PATCH 01/52] Remove unused video extension extraction in Share extension (#11081) Co-authored-by: Claude --- modules/Share-with-Bluesky/ShareViewController.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/modules/Share-with-Bluesky/ShareViewController.swift b/modules/Share-with-Bluesky/ShareViewController.swift index cd15fd1909..4eeab8876b 100644 --- a/modules/Share-with-Bluesky/ShareViewController.swift +++ b/modules/Share-with-Bluesky/ShareViewController.swift @@ -121,7 +121,6 @@ class ShareViewController: UIViewController { let firstItem = items.first if let dataUrl = try? await firstItem?.loadItem(forTypeIdentifier: "public.movie") as? URL { - let ext = String(dataUrl.lastPathComponent.split(separator: ".").last ?? "mp4") if let videoUriInfo = saveVideoWithInfo(dataUrl), let url = URL(string: "\(self.appScheme)://intent/compose?videoUri=\(videoUriInfo)") { _ = self.openURL(url) From c9251f85c555f8864a2d11c2cded0382ae8d5817 Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Thu, 9 Jul 2026 21:17:16 +0300 Subject: [PATCH 02/52] Update lint-staged (#11084) --- package.json | 2 +- pnpm-lock.yaml | 322 +++++++++++++++++++++---------------------------- 2 files changed, 140 insertions(+), 184 deletions(-) diff --git a/package.json b/package.json index 48b5eb246b..d50caa57ee 100644 --- a/package.json +++ b/package.json @@ -295,7 +295,7 @@ "jest": "^29.7.0", "jest-expo": "~54.0.17", "jest-junit": "^16.0.0", - "lint-staged": "^13.2.3", + "lint-staged": "^17.0.8", "prettier": "^3.8.3", "react-native-dotenv": "^3.4.11", "react-refresh": "^0.14.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f2c97f65df..f1cbc333bb 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -843,8 +843,8 @@ importers: specifier: ^16.0.0 version: 16.0.0 lint-staged: - specifier: ^13.2.3 - version: 13.3.0 + specifier: ^17.0.8 + version: 17.0.8 node: specifier: runtime:^24.18.0 version: runtime:24.18.0 @@ -4018,14 +4018,14 @@ packages: resolution: {integrity: sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==} engines: {node: '>=8'} - ansi-escapes@5.0.0: - resolution: {integrity: sha512-5GFMVX8HqE/TB+FuBJGuO5XG0WrsA6ptUqoODaT/n9mmUaZFkqnBueB4leqGBCmrUHnCnC4PCZTCd0E7QQ83bA==} - engines: {node: '>=12'} - ansi-escapes@6.2.1: resolution: {integrity: sha512-4nJ3yixlEthEJ9Rk4vPcdBRkZvQZlYyu8j4/Mqz5sgIkddmEnH2Yj2ZrnP9S3tQOvSNRUIgVNF/1yPpRAGNRig==} engines: {node: '>=14.16'} + ansi-escapes@7.3.0: + resolution: {integrity: sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg==} + engines: {node: '>=18'} + ansi-html-community@0.0.8: resolution: {integrity: sha512-1APHAyr3+PCamwNw3bXCPp4HFLONZt/yIH0sZp0/469KWNTEy+qN5jQ3GVX6DMZ1UXAi34yVwtTeaG/HpBuuzw==} engines: {'0': node >= 0.8.0} @@ -4395,10 +4395,6 @@ packages: resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} engines: {node: '>=10'} - chalk@5.3.0: - resolution: {integrity: sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==} - engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} - char-regex@1.0.2: resolution: {integrity: sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==} engines: {node: '>=10'} @@ -4459,9 +4455,9 @@ packages: resolution: {integrity: sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==} engines: {node: '>=8'} - cli-cursor@4.0.0: - resolution: {integrity: sha512-VGtlMu3x/4DOtIUwEkRezxUZ2lBacNJCHash0N0WeZDBS+7Ux1dm3XWAgWYxLJFMMdOeXMHXorshEFhbMSGelg==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + cli-cursor@5.0.0: + resolution: {integrity: sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==} + engines: {node: '>=18'} cli-spinners@2.9.2: resolution: {integrity: sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==} @@ -4471,9 +4467,9 @@ packages: resolution: {integrity: sha512-IqLQi4lO0nIB4tcdTpN4LCB9FI3uqrJZK7RC515EnhZ6qBaglkIgICb1wjeAqpdoOabm1+SuQtkXIPdYC93jhQ==} engines: {node: '>= 0.2.0'} - cli-truncate@3.1.0: - resolution: {integrity: sha512-wfOBkjXteqSnI59oPcJkcPl/ZmwvMMOj340qUIY1SKZCv0B9Cf4D4fAucRkIKQmsIuYK3x1rrgU7MeGRruiuiA==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + cli-truncate@5.2.0: + resolution: {integrity: sha512-xRwvIOMGrfOAnM1JYtqQImuaNtDEv9v6oIYAs4LIHwTiKee8uwvIi363igssOC0O5U04i4AlENs79LQLu9tEMw==} + engines: {node: '>=20'} cliui@6.0.0: resolution: {integrity: sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==} @@ -4535,10 +4531,6 @@ packages: resolution: {integrity: sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==} engines: {node: '>=14'} - commander@11.0.0: - resolution: {integrity: sha512-9HMlXtt/BNoYr8ooyjjNRdIilOTkVJXB+GhxMTtOKwk0R4j4lS4NpjuqmRxroBfnfTSHQIHQB7wryHhXarNjmQ==} - engines: {node: '>=16'} - commander@12.1.0: resolution: {integrity: sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==} engines: {node: '>=18'} @@ -4797,15 +4789,6 @@ packages: supports-color: optional: true - debug@4.3.4: - resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==} - engines: {node: '>=6.0'} - peerDependencies: - supports-color: '*' - peerDependenciesMeta: - supports-color: - optional: true - debug@4.4.3: resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} engines: {node: '>=6.0'} @@ -5042,6 +5025,10 @@ packages: resolution: {integrity: sha512-ObFo8v4rQJAE59M69QzwloxPZtd33TpYEIjtKD1rrFDcM1Gd7IkDxEBU+HriziN6HSHQnBJi8Dmy+JWkav5HKA==} engines: {node: '>=8'} + environment@1.1.0: + resolution: {integrity: sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==} + engines: {node: '>=18'} + error-ex@1.3.4: resolution: {integrity: sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==} @@ -5276,10 +5263,6 @@ packages: resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==} engines: {node: '>=10'} - execa@7.2.0: - resolution: {integrity: sha512-UduyVP7TLB5IcAQl+OzLyLcS/l32W/GLg+AhHJ+ow40FOk2U3SAllPwR44v4vmdFwIWqpdwxxpQbF1n5ta9seA==} - engines: {node: ^14.18.0 || ^16.14.0 || >=18.0.0} - exit@0.1.2: resolution: {integrity: sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==} engines: {node: '>= 0.8.0'} @@ -5789,6 +5772,10 @@ packages: resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} engines: {node: 6.* || 8.* || >= 10.*} + get-east-asian-width@1.6.0: + resolution: {integrity: sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==} + engines: {node: '>=18'} + get-intrinsic@1.3.0: resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} engines: {node: '>= 0.4'} @@ -6036,10 +6023,6 @@ packages: resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==} engines: {node: '>=10.17.0'} - human-signals@4.3.1: - resolution: {integrity: sha512-nZXjEF2nbo7lIw3mgYjItAfgQXog3OjJogSbKa2CQIIvSGWcKgeJnQlNXip6NglNzYH45nSRiEVimMvYL8DDqQ==} - engines: {node: '>=14.18.0'} - husky@9.1.7: resolution: {integrity: sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==} engines: {node: '>=18'} @@ -6205,9 +6188,9 @@ packages: resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} engines: {node: '>=8'} - is-fullwidth-code-point@4.0.0: - resolution: {integrity: sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==} - engines: {node: '>=12'} + is-fullwidth-code-point@5.1.0: + resolution: {integrity: sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ==} + engines: {node: '>=18'} is-generator-fn@2.1.0: resolution: {integrity: sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==} @@ -6288,10 +6271,6 @@ packages: resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} engines: {node: '>=8'} - is-stream@3.0.0: - resolution: {integrity: sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - is-string@1.1.1: resolution: {integrity: sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==} engines: {node: '>= 0.4'} @@ -6735,19 +6714,14 @@ packages: linkify-it@5.0.0: resolution: {integrity: sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==} - lint-staged@13.3.0: - resolution: {integrity: sha512-mPRtrYnipYYv1FEE134ufbWpeggNTo+O/UPzngoaKzbzHAthvR55am+8GfHTnqNRQVRRrYQLGW9ZyUoD7DsBHQ==} - engines: {node: ^16.14.0 || >=18.0.0} + lint-staged@17.0.8: + resolution: {integrity: sha512-B2P/d+jVW0UXOQ0MVMLrB/9ydA1P+zz6jYfdrbbEd9ur3S2rcbduFWKiUCC02Sm5hbC8nrm7y24WuYMG54HfxA==} + engines: {node: '>=22.22.1'} hasBin: true - listr2@6.6.1: - resolution: {integrity: sha512-+rAXGHh0fkEWdXBmX+L6mmfmXmXvDGEKzkjxO+8mP3+nI/r/CWznVBvsibXdxda9Zz0OW2e2ikphN3OwCT/jSg==} - engines: {node: '>=16.0.0'} - peerDependencies: - enquirer: '>= 2.3.0 < 3' - peerDependenciesMeta: - enquirer: - optional: true + listr2@10.2.2: + resolution: {integrity: sha512-JtNtbZj8q5BnDMR7trpwvwk3RIrANtIVzEUm8w7amp6xelLgyuq+4WZoTH913XaQAoH/cNdYhaNzBPA2U3xbDw==} + engines: {node: '>=22.13.0'} loader-runner@4.3.2: resolution: {integrity: sha512-DFEqQ3ihfS9blba08cLfYf1NRAIEm+dDjic073DRDc3/JspI/8wYmtDsHwd3+4hwvdxSK7PGaElfTmm0awWJ4w==} @@ -6801,9 +6775,9 @@ packages: resolution: {integrity: sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==} engines: {node: '>=10'} - log-update@5.0.1: - resolution: {integrity: sha512-5UtUDQ/6edw4ofyljDNcOVJQ4c7OjDro4h3y8e1GQL5iYElYclVHJ3zeWchylvMaKnDbDilC8irOVyexnA/Slw==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + log-update@6.1.0: + resolution: {integrity: sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==} + engines: {node: '>=18'} loose-envify@1.4.0: resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} @@ -6960,10 +6934,6 @@ packages: engines: {node: '>=20.19.4'} hasBin: true - micromatch@4.0.5: - resolution: {integrity: sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==} - engines: {node: '>=8.6'} - micromatch@4.0.8: resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} engines: {node: '>=8.6'} @@ -6993,9 +6963,9 @@ packages: resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} engines: {node: '>=6'} - mimic-fn@4.0.0: - resolution: {integrity: sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==} - engines: {node: '>=12'} + mimic-function@5.0.1: + resolution: {integrity: sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==} + engines: {node: '>=18'} min-indent@1.0.1: resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==} @@ -7055,9 +7025,6 @@ packages: ms@2.0.0: resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==} - ms@2.1.2: - resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} - ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} @@ -7273,10 +7240,6 @@ packages: resolution: {integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==} engines: {node: '>=8'} - npm-run-path@5.3.0: - resolution: {integrity: sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - nth-check@2.1.1: resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==} @@ -7351,9 +7314,9 @@ packages: resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} engines: {node: '>=6'} - onetime@6.0.0: - resolution: {integrity: sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==} - engines: {node: '>=12'} + onetime@7.0.0: + resolution: {integrity: sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==} + engines: {node: '>=18'} open@7.4.2: resolution: {integrity: sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q==} @@ -7465,10 +7428,6 @@ packages: resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} engines: {node: '>=8'} - path-key@4.0.0: - resolution: {integrity: sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==} - engines: {node: '>=12'} - path-parse@1.0.7: resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} @@ -7501,11 +7460,6 @@ packages: resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} engines: {node: '>=12'} - pidtree@0.6.0: - resolution: {integrity: sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g==} - engines: {node: '>=0.10'} - hasBin: true - pify@2.3.0: resolution: {integrity: sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==} engines: {node: '>=0.10.0'} @@ -8326,9 +8280,9 @@ packages: resolution: {integrity: sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==} engines: {node: '>=8'} - restore-cursor@4.0.0: - resolution: {integrity: sha512-I9fPXU9geO9bHOt9pHHOhOkYerIMsmVaWB0rA2AI9ERh/+x/i7MV5HKBNrg+ljO5eoPVgCcnFuRjJ9uH6I/3eg==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + restore-cursor@5.1.0: + resolution: {integrity: sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==} + engines: {node: '>=18'} retry@0.13.1: resolution: {integrity: sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==} @@ -8539,9 +8493,13 @@ packages: resolution: {integrity: sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg==} engines: {node: '>=14.16'} - slice-ansi@5.0.0: - resolution: {integrity: sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==} - engines: {node: '>=12'} + slice-ansi@7.1.2: + resolution: {integrity: sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w==} + engines: {node: '>=18'} + + slice-ansi@8.0.0: + resolution: {integrity: sha512-stxByr12oeeOyY2BlviTNQlYV5xOj47GirPr4yA1hE9JCtxfQN0+tVbkxwCtYDQWhEKWFHsEK48ORg5jrouCAg==} + engines: {node: '>=20'} slugify@1.6.9: resolution: {integrity: sha512-vZ7rfeehZui7wQs438JXBckYLkIIdfHOXsaVEUMyS5fHo1483l1bMdo0EDSWYclY0yZKFOipDy4KHuKs6ssvdg==} @@ -8684,6 +8642,14 @@ packages: resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} engines: {node: '>=12'} + string-width@7.2.0: + resolution: {integrity: sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==} + engines: {node: '>=18'} + + string-width@8.2.1: + resolution: {integrity: sha512-IIaP0g3iy9Cyy18w3M9YcaDudujEAVHKt3a3QJg1+sr/oX96TbaGUubG0hJyCjCBThFH+tFpcIyoUHUn1ogaLA==} + engines: {node: '>=20'} + string.prototype.matchall@4.0.12: resolution: {integrity: sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA==} engines: {node: '>= 0.4'} @@ -8729,10 +8695,6 @@ packages: resolution: {integrity: sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==} engines: {node: '>=6'} - strip-final-newline@3.0.0: - resolution: {integrity: sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==} - engines: {node: '>=12'} - strip-indent@3.0.0: resolution: {integrity: sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==} engines: {node: '>=8'} @@ -8887,6 +8849,10 @@ packages: tiny-worker@2.3.0: resolution: {integrity: sha512-pJ70wq5EAqTAEl9IkGzA+fN0836rycEuz2Cn6yeZ6FRzlVS5IDOkFHpIoEsksPRQV34GDqXm65+OlnZqUSyK2g==} + tinyexec@1.2.4: + resolution: {integrity: sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==} + engines: {node: '>=18'} + tinyglobby@0.2.16: resolution: {integrity: sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==} engines: {node: '>=12.0.0'} @@ -9328,6 +9294,10 @@ packages: resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} engines: {node: '>=0.10.0'} + wrap-ansi@10.0.0: + resolution: {integrity: sha512-SGcvg80f0wUy2/fXES19feHMz8E0JoXv2uNgHOu4Dgi2OrCy1lqwFYEJz1BLbDI0exjPMe/ZdzZ/YpGECBG/aQ==} + engines: {node: '>=20'} + wrap-ansi@6.2.0: resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==} engines: {node: '>=8'} @@ -9340,6 +9310,10 @@ packages: resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} engines: {node: '>=12'} + wrap-ansi@9.0.2: + resolution: {integrity: sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==} + engines: {node: '>=18'} + wrappy@1.0.2: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} @@ -9429,10 +9403,6 @@ packages: resolution: {integrity: sha512-vIYeF1u3CjlhAFekPPAk2h/Kv4T3mAkMox5OymRiJQB0spDP10LHvt+K7G9Ny6NuuMAb25/6n1qyUjAcGNf/AA==} engines: {node: '>= 6'} - yaml@2.3.1: - resolution: {integrity: sha512-2eHWfjaoXgTBC2jNM1LRef62VQa0umtvRiDSk6HSzW7RvS5YtkabJrwYLLEKWBc8a5U2PTSCs+dJjUTJdlHsWQ==} - engines: {node: '>= 14'} - yaml@2.9.0: resolution: {integrity: sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==} engines: {node: '>= 14.6'} @@ -13306,12 +13276,12 @@ snapshots: dependencies: type-fest: 0.21.3 - ansi-escapes@5.0.0: - dependencies: - type-fest: 1.4.0 - ansi-escapes@6.2.1: {} + ansi-escapes@7.3.0: + dependencies: + environment: 1.1.0 + ansi-html-community@0.0.8: {} ansi-html@0.0.9: {} @@ -13773,8 +13743,6 @@ snapshots: ansi-styles: 4.3.0 supports-color: 7.2.0 - chalk@5.3.0: {} - char-regex@1.0.2: {} char-regex@2.0.2: {} @@ -13850,9 +13818,9 @@ snapshots: dependencies: restore-cursor: 3.1.0 - cli-cursor@4.0.0: + cli-cursor@5.0.0: dependencies: - restore-cursor: 4.0.0 + restore-cursor: 5.1.0 cli-spinners@2.9.2: {} @@ -13860,10 +13828,10 @@ snapshots: dependencies: colors: 1.0.3 - cli-truncate@3.1.0: + cli-truncate@5.2.0: dependencies: - slice-ansi: 5.0.0 - string-width: 5.1.2 + slice-ansi: 8.0.0 + string-width: 8.2.1 cliui@6.0.0: dependencies: @@ -13919,8 +13887,6 @@ snapshots: commander@10.0.1: {} - commander@11.0.0: {} - commander@12.1.0: {} commander@2.20.0: {} @@ -14206,10 +14172,6 @@ snapshots: dependencies: ms: 2.1.3 - debug@4.3.4: - dependencies: - ms: 2.1.2 - debug@4.4.3: dependencies: ms: 2.1.3 @@ -14398,6 +14360,8 @@ snapshots: env-editor@0.4.2: {} + environment@1.1.0: {} + error-ex@1.3.4: dependencies: is-arrayish: 0.2.1 @@ -14772,18 +14736,6 @@ snapshots: signal-exit: 3.0.7 strip-final-newline: 2.0.0 - execa@7.2.0: - dependencies: - cross-spawn: 7.0.6 - get-stream: 6.0.1 - human-signals: 4.3.1 - is-stream: 3.0.0 - merge-stream: 2.0.0 - npm-run-path: 5.3.0 - onetime: 6.0.0 - signal-exit: 3.0.7 - strip-final-newline: 3.0.0 - exit@0.1.2: {} expect@29.7.0: @@ -15378,6 +15330,8 @@ snapshots: get-caller-file@2.0.5: {} + get-east-asian-width@1.6.0: {} + get-intrinsic@1.3.0: dependencies: call-bind-apply-helpers: 1.0.2 @@ -15667,8 +15621,6 @@ snapshots: human-signals@2.1.0: {} - human-signals@4.3.1: {} - husky@9.1.7: {} hyphenate-style-name@1.1.0: {} @@ -15818,7 +15770,9 @@ snapshots: is-fullwidth-code-point@3.0.0: {} - is-fullwidth-code-point@4.0.0: {} + is-fullwidth-code-point@5.1.0: + dependencies: + get-east-asian-width: 1.6.0 is-generator-fn@2.1.0: {} @@ -15885,8 +15839,6 @@ snapshots: is-stream@2.0.1: {} - is-stream@3.0.0: {} - is-string@1.1.1: dependencies: call-bound: 1.0.4 @@ -16558,30 +16510,22 @@ snapshots: dependencies: uc.micro: 2.1.0 - lint-staged@13.3.0: + lint-staged@17.0.8: dependencies: - chalk: 5.3.0 - commander: 11.0.0 - debug: 4.3.4 - execa: 7.2.0 - lilconfig: 2.1.0 - listr2: 6.6.1 - micromatch: 4.0.5 - pidtree: 0.6.0 + listr2: 10.2.2 + picomatch: 4.0.4 string-argv: 0.3.2 - yaml: 2.3.1 - transitivePeerDependencies: - - enquirer - - supports-color + tinyexec: 1.2.4 + optionalDependencies: + yaml: 2.9.0 - listr2@6.6.1: + listr2@10.2.2: dependencies: - cli-truncate: 3.1.0 - colorette: 2.0.20 + cli-truncate: 5.2.0 eventemitter3: 5.0.4 - log-update: 5.0.1 + log-update: 6.1.0 rfdc: 1.4.1 - wrap-ansi: 8.1.0 + wrap-ansi: 10.0.0 loader-runner@4.3.2: {} @@ -16629,13 +16573,13 @@ snapshots: chalk: 4.1.2 is-unicode-supported: 0.1.0 - log-update@5.0.1: + log-update@6.1.0: dependencies: - ansi-escapes: 5.0.0 - cli-cursor: 4.0.0 - slice-ansi: 5.0.0 + ansi-escapes: 7.3.0 + cli-cursor: 5.0.0 + slice-ansi: 7.1.2 strip-ansi: 7.2.0 - wrap-ansi: 8.1.0 + wrap-ansi: 9.0.2 loose-envify@1.4.0: dependencies: @@ -16900,11 +16844,6 @@ snapshots: - supports-color - utf-8-validate - micromatch@4.0.5: - dependencies: - braces: 3.0.3 - picomatch: 2.3.2 - micromatch@4.0.8: dependencies: braces: 3.0.3 @@ -16924,7 +16863,7 @@ snapshots: mimic-fn@2.1.0: {} - mimic-fn@4.0.0: {} + mimic-function@5.0.1: {} min-indent@1.0.1: {} @@ -16970,8 +16909,6 @@ snapshots: ms@2.0.0: {} - ms@2.1.2: {} - ms@2.1.3: {} multicast-dns@7.2.5: @@ -17049,10 +16986,6 @@ snapshots: dependencies: path-key: 3.1.1 - npm-run-path@5.3.0: - dependencies: - path-key: 4.0.0 - nth-check@2.1.1: dependencies: boolbase: 1.0.0 @@ -17132,9 +17065,9 @@ snapshots: dependencies: mimic-fn: 2.1.0 - onetime@6.0.0: + onetime@7.0.0: dependencies: - mimic-fn: 4.0.0 + mimic-function: 5.0.1 open@7.4.2: dependencies: @@ -17259,8 +17192,6 @@ snapshots: path-key@3.1.1: {} - path-key@4.0.0: {} - path-parse@1.0.7: {} path-scurry@1.11.1: @@ -17285,8 +17216,6 @@ snapshots: picomatch@4.0.4: {} - pidtree@0.6.0: {} - pify@2.3.0: {} pify@4.0.1: {} @@ -18249,10 +18178,10 @@ snapshots: onetime: 5.1.2 signal-exit: 3.0.7 - restore-cursor@4.0.0: + restore-cursor@5.1.0: dependencies: - onetime: 5.1.2 - signal-exit: 3.0.7 + onetime: 7.0.0 + signal-exit: 4.1.0 retry@0.13.1: {} @@ -18491,10 +18420,15 @@ snapshots: slash@5.1.0: {} - slice-ansi@5.0.0: + slice-ansi@7.1.2: dependencies: ansi-styles: 6.2.3 - is-fullwidth-code-point: 4.0.0 + is-fullwidth-code-point: 5.1.0 + + slice-ansi@8.0.0: + dependencies: + ansi-styles: 6.2.3 + is-fullwidth-code-point: 5.1.0 slugify@1.6.9: {} @@ -18639,6 +18573,17 @@ snapshots: emoji-regex: 9.2.2 strip-ansi: 7.2.0 + string-width@7.2.0: + dependencies: + emoji-regex: 10.6.0 + get-east-asian-width: 1.6.0 + strip-ansi: 7.2.0 + + string-width@8.2.1: + dependencies: + get-east-asian-width: 1.6.0 + strip-ansi: 7.2.0 + string.prototype.matchall@4.0.12: dependencies: call-bind: 1.0.9 @@ -18707,8 +18652,6 @@ snapshots: strip-final-newline@2.0.0: {} - strip-final-newline@3.0.0: {} - strip-indent@3.0.0: dependencies: min-indent: 1.0.1 @@ -18852,6 +18795,8 @@ snapshots: esm: 3.2.25 optional: true + tinyexec@1.2.4: {} + tinyglobby@0.2.16: dependencies: fdir: 6.5.0(picomatch@4.0.4) @@ -18914,7 +18859,8 @@ snapshots: type-fest@0.7.1: {} - type-fest@1.4.0: {} + type-fest@1.4.0: + optional: true type-is@1.6.18: dependencies: @@ -19372,6 +19318,12 @@ snapshots: word-wrap@1.2.5: {} + wrap-ansi@10.0.0: + dependencies: + ansi-styles: 6.2.3 + string-width: 8.2.1 + strip-ansi: 7.2.0 + wrap-ansi@6.2.0: dependencies: ansi-styles: 4.3.0 @@ -19390,6 +19342,12 @@ snapshots: string-width: 5.1.2 strip-ansi: 7.2.0 + wrap-ansi@9.0.2: + dependencies: + ansi-styles: 6.2.3 + string-width: 7.2.0 + strip-ansi: 7.2.0 + wrappy@1.0.2: {} write-file-atomic@4.0.2: @@ -19437,8 +19395,6 @@ snapshots: yaml@1.10.3: {} - yaml@2.3.1: {} - yaml@2.9.0: {} yargs-parser@18.1.3: From b075753d0f08bef40f5118349fbc74b8c5e8a841 Mon Sep 17 00:00:00 2001 From: surfdude29 <149612116+surfdude29@users.noreply.github.com> Date: Thu, 9 Jul 2026 19:19:41 +0100 Subject: [PATCH 03/52] Fix KWS language picker silently resetting Arabic, Chinese, Filipino & Portuguese to English (#11082) Co-authored-by: Claude Co-authored-by: Eric Bailey --- src/components/LanguageSelect.tsx | 26 +++++++++++-------- .../ageAssurance/AgeAssuranceInitDialog.tsx | 1 + 2 files changed, 16 insertions(+), 11 deletions(-) diff --git a/src/components/LanguageSelect.tsx b/src/components/LanguageSelect.tsx index 30b1143091..6bcaebe539 100644 --- a/src/components/LanguageSelect.tsx +++ b/src/components/LanguageSelect.tsx @@ -1,4 +1,3 @@ -import {useCallback} from 'react' import {msg} from '@lingui/core/macro' import {useLingui} from '@lingui/react' @@ -14,26 +13,31 @@ export function LanguageSelect({ value: l.code2, })), label, + disabledBlueskySupportedLanguageSanitization = false, }: { value?: string onChange: (value: string) => void items?: {label: string; value: string}[] label?: string + disabledBlueskySupportedLanguageSanitization?: boolean }) { const {_} = useLingui() + const selectValue = + value && !disabledBlueskySupportedLanguageSanitization + ? sanitizeAppLanguageSetting(value) + : value - const handleOnChange = useCallback( - (value: string) => { - if (!value) return - onChange(sanitizeAppLanguageSetting(value)) - }, - [onChange], - ) + const handleOnChange = (value: string) => { + if (!value) return + onChange( + disabledBlueskySupportedLanguageSanitization + ? value + : sanitizeAppLanguageSetting(value), + ) + } return ( - + diff --git a/src/components/ageAssurance/AgeAssuranceInitDialog.tsx b/src/components/ageAssurance/AgeAssuranceInitDialog.tsx index 15e3a2b472..c92d43e32c 100644 --- a/src/components/ageAssurance/AgeAssuranceInitDialog.tsx +++ b/src/components/ageAssurance/AgeAssuranceInitDialog.tsx @@ -296,6 +296,7 @@ function Inner() { setLanguageError(false) }} items={KWS_SUPPORTED_LANGS} + disabledBlueskySupportedLanguageSanitization /> {languageError && ( From 1d1b2f748e27a8029d895f85872f3aa8868e94e1 Mon Sep 17 00:00:00 2001 From: surfdude29 <149612116+surfdude29@users.noreply.github.com> Date: Thu, 9 Jul 2026 19:24:20 +0100 Subject: [PATCH 04/52] Update KWS supported languages and mapping (#11085) Co-authored-by: Claude Fable 5 --- .../ageAssurance/AgeAssuranceInitDialog.tsx | 5 ++- src/components/ageAssurance/const.ts | 37 +++++++++++++------ 2 files changed, 29 insertions(+), 13 deletions(-) diff --git a/src/components/ageAssurance/AgeAssuranceInitDialog.tsx b/src/components/ageAssurance/AgeAssuranceInitDialog.tsx index c92d43e32c..9d74b0c6dc 100644 --- a/src/components/ageAssurance/AgeAssuranceInitDialog.tsx +++ b/src/components/ageAssurance/AgeAssuranceInitDialog.tsx @@ -343,11 +343,12 @@ function convertToKWSSupportedLanguage( // pt-PT is pt (pt-BR is supported independently) case 'pt-PT': return 'pt' - // only chinese (simplified) is supported, map all chinese variants + // map Chinese variants to simplified or traditional case 'zh-Hans-CN': + return 'zh-Hans' case 'zh-Hant-HK': case 'zh-Hant-TW': - return 'zh-Hans' + return 'zh-Hant' default: // try and map directly - if undefined, they will have to pick from the dropdown return KWS_SUPPORTED_LANGS.find(v => v.value === appLanguage)?.value diff --git a/src/components/ageAssurance/const.ts b/src/components/ageAssurance/const.ts index 93ff0f4152..2572733c4c 100644 --- a/src/components/ageAssurance/const.ts +++ b/src/components/ageAssurance/const.ts @@ -7,21 +7,36 @@ export const urls = { export const KWS_SUPPORTED_LANGS = [ {value: 'en', label: 'English'}, {value: 'ar', label: 'العربية'}, - {value: 'zh-Hans', label: '简体中文'}, - {value: 'cs', label: 'Čeština'}, - {value: 'nl', label: 'Nederlands'}, - {value: 'tl', label: 'Filipino'}, - {value: 'fr', label: 'Français'}, + {value: 'bg', label: 'български'}, + {value: 'cs', label: 'čeština'}, + {value: 'da', label: 'dansk'}, {value: 'de', label: 'Deutsch'}, + {value: 'el', label: 'Ελληνικά'}, + {value: 'es', label: 'español'}, + {value: 'es-MX', label: 'español de México'}, + {value: 'fi', label: 'suomi'}, + {value: 'fil', label: 'Filipino'}, + {value: 'fr', label: 'français'}, + {value: 'hi', label: 'हिंदी'}, + {value: 'hu', label: 'magyar'}, {value: 'id', label: 'Bahasa Indonesia'}, - {value: 'it', label: 'Italiano'}, + {value: 'it', label: 'italiano'}, {value: 'ja', label: '日本語'}, {value: 'ko', label: '한국어'}, - {value: 'pt', label: 'Português'}, - {value: 'pt-BR', label: 'Português (Brasil)'}, - {value: 'ru', label: 'Русский'}, - {value: 'es', label: 'Español'}, - {value: 'tr', label: 'Türkçe'}, + {value: 'ms', label: 'Bahasa Melayu'}, + {value: 'nl', label: 'Nederlands'}, + {value: 'no', label: 'norsk'}, + {value: 'pl', label: 'polski'}, + {value: 'pt', label: 'português'}, + {value: 'pt-BR', label: 'português do Brasil'}, + {value: 'ro', label: 'română'}, + {value: 'ru', label: 'русский'}, + {value: 'sv', label: 'svenska'}, {value: 'th', label: 'ภาษาไทย'}, + {value: 'tl', label: 'Tagalog'}, + {value: 'tr', label: 'Türkçe'}, + {value: 'uk', label: 'українська'}, {value: 'vi', label: 'Tiếng Việt'}, + {value: 'zh-Hans', label: '简体中文'}, + {value: 'zh-Hant', label: '繁體中文'}, ] From ffd4172a64b11daf3e84006286217cefe301a0a4 Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Fri, 10 Jul 2026 00:37:05 +0300 Subject: [PATCH 05/52] Replace @tiptap/html generateJSON with native DOMParser equivalent (#11098) --- package.json | 1 - pnpm-lock.yaml | 30 --------------- .../com/composer/text-input/TextInput.web.tsx | 37 ++++++++++++++++--- 3 files changed, 31 insertions(+), 37 deletions(-) diff --git a/package.json b/package.json index d50caa57ee..7a46301e8d 100644 --- a/package.json +++ b/package.json @@ -140,7 +140,6 @@ "@tiptap/extension-paragraph": "^2.9.1", "@tiptap/extension-placeholder": "^2.9.1", "@tiptap/extension-text": "^2.9.1", - "@tiptap/html": "^2.9.1", "@tiptap/pm": "^2.9.1", "@tiptap/react": "^2.9.1", "@tiptap/suggestion": "^2.9.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f1cbc333bb..6840d80184 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -382,9 +382,6 @@ importers: '@tiptap/extension-text': specifier: ^2.9.1 version: 2.27.2(@tiptap/core@2.27.2(@tiptap/pm@2.27.2)) - '@tiptap/html': - specifier: ^2.9.1 - version: 2.27.2(@tiptap/core@2.27.2(@tiptap/pm@2.27.2))(@tiptap/pm@2.27.2) '@tiptap/pm': specifier: ^2.9.1 version: 2.27.2 @@ -3450,12 +3447,6 @@ packages: peerDependencies: '@tiptap/core': ^2.7.0 - '@tiptap/html@2.27.2': - resolution: {integrity: sha512-WiZgAvFjUprjyAczxAHLN9k++w7klwsCJ7EJDljtW/QXQ476Q0GqNtaHG4WRuW25cBH7c7AWZFptALeak2OE4Q==} - peerDependencies: - '@tiptap/core': ^2.7.0 - '@tiptap/pm': ^2.7.0 - '@tiptap/pm@2.27.2': resolution: {integrity: sha512-kaEg7BfiJPDQMKbjVIzEPO3wlcA+pZb2tlcK9gPrdDnEFaec2QTF1sXz2ak2IIb2curvnIrQ4yrfHgLlVA72wA==} @@ -5013,10 +5004,6 @@ packages: resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} engines: {node: '>=0.12'} - entities@5.0.0: - resolution: {integrity: sha512-BeJFvFRJddxobhvEdm5GqHzRV/X+ACeuw0/BuuxsCh1EUZcAIz8+kYmBp/LrQuloy6K1f3a0M7+IhmZ7QnkISA==} - engines: {node: '>=0.12'} - entities@6.0.1: resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==} engines: {node: '>=0.12'} @@ -9432,10 +9419,6 @@ packages: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} - zeed-dom@0.15.1: - resolution: {integrity: sha512-dtZ0aQSFyZmoJS0m06/xBN1SazUBPL5HpzlAcs/KcRW0rzadYw12deQBjeMhGKMMeGEp7bA9vmikMLaO4exBcg==} - engines: {node: '>=14.13.1'} - zod-validation-error@3.5.4: resolution: {integrity: sha512-+hEiRIiPobgyuFlEojnqjJnhFvg4r/i3cqgcm67eehZf/WBaK3g6cD02YU9mtdVxZjv8CzCA9n/Rhrs3yAAvAw==} engines: {node: '>=18.0.0'} @@ -12653,12 +12636,6 @@ snapshots: dependencies: '@tiptap/core': 2.27.2(@tiptap/pm@2.27.2) - '@tiptap/html@2.27.2(@tiptap/core@2.27.2(@tiptap/pm@2.27.2))(@tiptap/pm@2.27.2)': - dependencies: - '@tiptap/core': 2.27.2(@tiptap/pm@2.27.2) - '@tiptap/pm': 2.27.2 - zeed-dom: 0.15.1 - '@tiptap/pm@2.27.2': dependencies: prosemirror-changeset: 2.4.1 @@ -14354,8 +14331,6 @@ snapshots: entities@4.5.0: {} - entities@5.0.0: {} - entities@6.0.1: {} env-editor@0.4.2: {} @@ -19435,11 +19410,6 @@ snapshots: yocto-queue@0.1.0: {} - zeed-dom@0.15.1: - dependencies: - css-what: 6.2.2 - entities: 5.0.0 - zod-validation-error@3.5.4(zod@3.25.76): dependencies: zod: 3.25.76 diff --git a/src/view/com/composer/text-input/TextInput.web.tsx b/src/view/com/composer/text-input/TextInput.web.tsx index bb314f116c..e76d9e3f3d 100644 --- a/src/view/com/composer/text-input/TextInput.web.tsx +++ b/src/view/com/composer/text-input/TextInput.web.tsx @@ -10,6 +10,7 @@ import {StyleSheet, View} from 'react-native' import Animated, {FadeIn, FadeOut} from 'react-native-reanimated' import {AppBskyRichtextFacet, RichText} from '@atproto/api' import {Trans} from '@lingui/react/macro' +import {getSchema} from '@tiptap/core' import {Document} from '@tiptap/extension-document' import Hardbreak from '@tiptap/extension-hard-break' import History from '@tiptap/extension-history' @@ -17,8 +18,12 @@ import {Mention} from '@tiptap/extension-mention' import {Paragraph} from '@tiptap/extension-paragraph' import {Placeholder} from '@tiptap/extension-placeholder' import {Text as TiptapText} from '@tiptap/extension-text' -import {generateJSON} from '@tiptap/html' -import {Fragment, Node, Slice} from '@tiptap/pm/model' +import { + DOMParser as ProseMirrorDOMParser, + Fragment, + Node, + Slice, +} from '@tiptap/pm/model' import {EditorContent, type JSONContent, useEditor} from '@tiptap/react' import {splitGraphemes} from 'unicode-segmenter/grapheme' @@ -242,9 +247,7 @@ export function TextInput({ } }, }, - content: generateJSON(richTextToHTML(richtext), extensions, { - preserveWhitespace: 'full', - }), + content: richTextToEditorJson(richtext, extensions), autofocus: autoFocus ? 'end' : null, editable: true, injectCSS: true, @@ -381,6 +384,26 @@ export function TextInput({ * * It also escapes HTML characters */ +/* + * Equivalent of @tiptap/html's generateJSON, but using the browser's native + * DOMParser. The @tiptap/html version supports SSR via zeed-dom, which pulls + * ~250KB of parser dependencies into the bundle - we are always in a browser + * here, so we don't need it. + */ +function richTextToEditorJson( + richtext: RichText, + extensions: Parameters[0], +): JSONContent { + const schema = getSchema(extensions) + const dom = new DOMParser().parseFromString( + richTextToHTML(richtext), + 'text/html', + ).body + return ProseMirrorDOMParser.fromSchema(schema) + .parse(dom, {preserveWhitespace: 'full'}) + .toJSON() +} + function richTextToHTML(richtext: RichText): string { let html = '' @@ -392,7 +415,9 @@ function richTextToHTML(richtext: RichText): string { } } - return html + // the body wrapper preserves leading whitespace, which the parser + // otherwise strips + return `${html}` } function escapeHTML(str: string): string { From 65e10f13f7883036073d4a80a23825c8a11cae2c Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Fri, 10 Jul 2026 00:37:48 +0300 Subject: [PATCH 06/52] Slim down CLAUDE.md (#11102) --- CLAUDE.md | 538 ++++++++++-------------------------------------------- 1 file changed, 95 insertions(+), 443 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 3a2c589dfc..e26e9e3f86 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -82,7 +82,7 @@ should go in `/screens` (not `/view/screens`) to encourage better organization and separation from legacy code. For complex screens that have specific components or data needs that _are not -shared by other screens_, we encourage subdirectoreis within `/screens/` +shared by other screens_, we encourage subdirectories within `/screens/` e.g. `/screens/ProfileScreen/ProfileScreen.tsx` and `/screens/ProfileScreen/components/`. @@ -126,44 +126,14 @@ eventually. Typically JS style for variables, functions, etc. We use ProudCamelCase for components, and camelCase directories and files. -When organizing new code, consider if it fits into a single file, or if it -should be broken down into multiple files. For "macro" component cases, or -things that live in `/features` or `/screens`, we often follow a pattern of -having an `index.tsx` for the main component, and then co-locating related -components, hooks, and utilities in the same directory. For example: +For "macro" cases in `/features`, `/screens`, or `/components`, co-locate related +code in a directory with an `index.tsx` main component plus sibling +components/hooks/utils (e.g. `screens/ProfileScreen/index.tsx` + +`screens/ProfileScreen/components/`). Keep related code together so it lives where +someone would look for it. Don't overdo it: a component that fits in one file +should just be `Component.tsx`, not `Component/index.tsx`. -``` -src -├── screens/ -│ ├── ProfileScreen/ -│ │ ├── index.tsx # Main screen component -│ │ ├── components/ # Sub-components used only by this screen -``` - -Similar patterns can be found in `/features` and `/components`. The idea here is -to keep related code together and make it easier to navigate. - -You should ask yourself: if someone new was looking for the code related to this -feature or screen, where would they expect to find it? Organizing code in a way -that matches developer expectations can make the codebase much more -approachable. Being able to say "Live Now stuff lives in `/features/liveNow`" is -easier to understand than having it scattered across multiple directories. - -No need to go overboard with this. If a component or feature fits into a single -file, there's no reason to have a `/Component/index.tsx` file when it could just -be `/Component.tsx`. Use your judgment based on the complexity and amount of -related code. - -#### Platform Specific Files - -We have conflicting patterns in the app for this. The preferred approach is to -group platform-specific files into a directory as much as possible. For example, -rather than having `Component.tsx`, `Component.web.tsx`, and -`Component.native.tsx` in the same directory, we prefer to have a `Component/` -directory with `index.tsx`, `index.web.tsx`, and `index.native.tsx`. This keeps -related code together and gives us a better visual cue that there are probably -other files contained within this "macro" feature, whereas `Component.tsx` on -its own looks more like a single component file. +Platform-specific files are covered under "Platform-Specific Code" below. ### Comments @@ -204,88 +174,40 @@ const fallbackDate = maximumDate ? toSimpleDateString(maximumDate) : today ### Documentation and Tests Within Features -For larger features or components, it's helpful to include a README.md file -within the directory that explains the purpose of the feature, how it works, and -any important implementation details. The `/Component/index.tsx` pattern lends -itself well to this, since the `index.tsx` can be the main component file, and -the `README.md` can provide documentation for the whole feature. This is -optional, but can be a nice way to keep documentation close to the code it -describes. - -Similarly, if there are tests that are specific to a component or feature, it -can be helpful to include them in the same directory, either as -`Component.test.tsx` or in a `__tests__/` subdirectory. This keeps everything -related to the component or feature in one place and makes it easier to find and -maintain tests. +For larger features or components, co-locate documentation and tests with the +code. A `README.md` in the directory (the `/Component/index.tsx` pattern lends +itself well to this) can document the whole feature, and feature-specific tests +belong alongside it as `Component.test.tsx` or in a `__tests__/` subdirectory. +Both are optional. ## Styling System (ALF) -ALF is the custom design system. It uses Tailwind-inspired naming with underscores instead of hyphens. +ALF is the custom design system. Tailwind-inspired naming with underscores +instead of hyphens. Static atoms (`atoms as a`) are theme-independent; theme +atoms/palette come from `useTheme()` (`t.atoms.bg`, `t.palette.primary_500`). +Style props take an array of atoms + theme atoms + raw styles. -### Basic Usage - -Generally, order atoms by: - -- Flexbox configuration, e.g., `a.flex_row` -- Spacing, e.g., `a.px_md` -- Text styles, e.g., `a.font_bold` -- Themes, e.g., `t.atoms.text`, -- Raw styles, e.g., `{backgroundColor: t.palette.primary_500}` +Order atoms by: flexbox (`a.flex_row`), spacing (`a.px_md`), text (`a.font_bold`), +themes (`t.atoms.text`), then raw styles (`{backgroundColor: t.palette.primary_500}`). ```tsx import {atoms as a, useTheme} from '#/alf' -function MyComponent() { - const t = useTheme() - - return ( - - Hello - - ) -} +const t = useTheme() + ``` ### Key Concepts -**Static Atoms** – Theme-independent styles imported from `atoms`: +Static atoms live in `a.*` (e.g. `a.flex_row`, `a.p_md`, `a.rounded_md`, +`a.text_lg`). Theme atoms/palette come from `useTheme()` (`t.atoms.bg`, +`t.atoms.text`, `t.atoms.border_contrast_low`, `t.palette.primary_500`). -```tsx -import {atoms as a} from '#/alf' -// a.flex_row, a.p_md, a.gap_sm, a.rounded_md, a.text_lg, etc. -``` +**Platform utilities** (`import {web, native, ios, android, platform} from '#/alf'`) +return conditional styles inline in a style array: `web({cursor: 'pointer'})`, +`native({paddingBottom: 20})`, `platform({ios: {...}, android: {...}, web: {...}})`. -**Theme Atoms** – Theme-dependent colors from `useTheme()`: - -```tsx -const t = useTheme() -// t.atoms.bg, t.atoms.text, t.atoms.border_contrast_low, etc. -// t.palette.primary_500, t.palette.negative_400, etc. -``` - -**Platform Utilities** – For platform-specific styles: - -```tsx -import {web, native, ios, android, platform} from '#/alf' - -const styles = [ - a.p_md, - web({cursor: 'pointer'}), - native({paddingBottom: 20}), - platform({ios: {...}, android: {...}, web: {...}}), -] -``` - -**Breakpoints** – Responsive design: - -```tsx -import {useBreakpoints} from '#/alf' - -const {gtPhone, gtMobile, gtTablet} = useBreakpoints() -if (gtMobile) { - // Tablet or desktop layout -} -``` +**Breakpoints:** `const {gtPhone, gtMobile, gtTablet} = useBreakpoints()` from `#/alf`. ### Naming Conventions @@ -306,176 +228,67 @@ if (gtMobile) { ```tsx import {Fragment} from 'react' import {View} from 'react-native' -import {Trans, useLingui} from '@lingui/react/macro' +import {Trans} from '@lingui/react/macro' import {Text} from '#/components/Typography' -function MyComponent({foo = []}: {foo?: string[]}) { - const {t: l} = useLingui() - +function MyComponent({items = []}: {items?: string[]}) { return ( <> - Example - {foo.map((foo, index) => ( - + + Example + + + + {items.map((item, index) => ( + {index} - {foo} + {item} ))} - ); + ) } ``` ### Dialog Component -Dialogs use a bottom sheet on native and a modal on web. Use `useDialogControl()` hook to manage state. - -```tsx -import * as Dialog from '#/components/Dialog' - -function MyFeature() { - const control = Dialog.useDialogControl() - - return ( - <> - - - - {/* Typically the inner part is in its own component */} - - - - ) -} - -function DialogInner() { - return ( - <> - {/* Native-only drag handle */} - - - Title - - Dialog content here - - {/* Web-only X button in top left */} - - - ) -} -``` +Lives in `#/components/Dialog`. Bottom sheet on native, modal on web. Manage +state with `useDialogControl()`. `Dialog.Handle` renders native-only, `Dialog.Close` +web-only. CRITICAL: run any post-close action inside the `control.close(() => ...)` +callback (see Footguns). Compound-component usage; canonical example in any dialog +under `#/components`. ### Menu Component -Menus render as a dropdown on web and a bottom sheet dialog on native. - -```tsx -import * as Menu from '#/components/Menu' - -function MyMenu() { - return ( - - - {({props}) => ( - - )} - - - - - - - Edit - - - - Delete - - - - - ) -} -``` +Lives in `#/components/Menu`. Dropdown on web, bottom sheet dialog on native. +`Menu.Divider` is web-only, `Menu.ContainerItem` native-only. Compound API +(`Menu.Root` / `Menu.Trigger` / `Menu.Outer` / `Menu.Group` / `Menu.Item`); grep +existing usages across the app for a canonical example. ### Button Component -```tsx -import {Button, ButtonText, ButtonIcon} from '#/components/Button' - -// Solid primary button (most common) - - -// With icon - - -// Icon-only button - - -// Ghost variant (deprecated - use color prop) - -``` - -**Button Props:** +`import {Button, ButtonText, ButtonIcon} from '#/components/Button'`. Props: - `color`: `'primary'` | `'secondary'` | `'negative'` | `'primary_subtle'` | `'negative_subtle'` | `'secondary_inverted'` - `size`: `'tiny'` | `'small'` | `'large'` - `shape`: `'default'` (pill) | `'round'` | `'square'` | `'rectangular'` -- `variant`: `'solid'` | `'outline'` | `'ghost'` (deprecated, use `color`) - -### Typography - -```tsx -import {Text, H1, H2, P} from '#/components/Typography' - -

Heading

-

Paragraph text with default styling.

-Custom text - -// For text with emoji, add the emoji prop. User-generated text (e.g. display names) -// will almost certainly contain emoji, so only omit it when the text is static and -// does not contain an emoji -Hello! 👋 -``` - -The `Text` component's default style is `[a.text_sm, a.leading_snug, t.atoms.text]`. +- `variant`: `'solid'` | `'outline'` | `'ghost'` (deprecated, prefer `color`) ### TextField -```tsx -import * as TextField from '#/components/forms/TextField' +Compound component at `#/components/forms/TextField` (`TextField.LabelText`, +`TextField.Root`, `TextField.Icon`, `TextField.Input`). Prefer `defaultValue` over +`value` (see Footguns). -Email - - - - -``` +### Typography + +`import {Text, H1, H2, P} from '#/components/Typography'`. The `Text` default style +is `[a.text_sm, a.leading_snug, t.atoms.text]`. Pass the `emoji` prop to any `Text` +that may contain emoji - user-generated text (display names etc.) almost always +does, so only omit it for static, emoji-free strings: `Hello!`. ## Internationalization (i18n) @@ -549,201 +362,49 @@ pnpm intl:compile # Compile translations for runtime ### TanStack Query (Data Fetching) -```tsx -// src/state/queries/profile.ts -import {useQuery, useMutation, useQueryClient} from '@tanstack/react-query' +Follow the established pattern in `src/state/queries/`; `src/state/queries/feed.ts` +is a good canonical reference (it uses `createQueryKey`, matching key roots, +`useInfiniteQuery`, and `persistedVersion`). -import {createQueryKey} from '#/state/queries/util' - -/* - * Query key name should match the query hook name for consistency - */ -const profileQueryKeyRoot = 'profile' - -/* - * Use object params and createQueryKey helper for better readability and to - * avoid bugs with parameter order or types. - */ -export const createProfileQueryKey = (args: {did: string}) => - createQueryKey(profileQueryKeyRoot, args) - -/* - * Query hook should be named use[Name]Query, where [Name] describes the data - * being fetched. This is not a strict requirement, but it's a helpful - * convention for discoverability - */ -export function useProfileQuery({did}: {did: string}) { - const agent = useAgent() - - return useQuery({ - queryKey: createProfileQueryKey({did}), - queryFn: async () => { - const res = await agent.getProfile({actor: did}) - return res.data - }, - staleTime: STALE.MINUTES.FIVE, - enabled: !!did, - }) -} - -/* - * Mutation hook should match the name of the query hook, but with "Mutation" - * suffix. This is not a strict requirement, but it's a helpful convention for - * discoverability and consistency. - */ -export function useProfileMutation() { - const queryClient = useQueryClient() - - return useMutation({ - mutationFn: async data => { - // Update logic - }, - onSuccess: (_, variables) => { - queryClient.invalidateQueries({ - queryKey: createProfileQueryKey({did: variables.did}), - }) - }, - onError: error => { - if (isNetworkError(error)) { - // don't log, but inform user - } else if (error instanceof AppBskyExampleProcedure.ExampleError) { - // XRPC APIs often have typed errors, allows nicer handling - } else { - // Log unexpected errors to Sentry - logger.error('Error updating profile', {safeMessage: error}) - } - }, - }) -} - -/* - * If cache mutation is needed, include specific interfaces for the specific - * mutations you require adjacent to the source queries. Naming should be - * descriptive of the mutation's purpose, e.g. use[Name]CacheMutation. This is - * not a strict requirement, but it's a helpful convention for discoverability - * and consistency. - */ -export function useProfileCacheMutation() { - const queryClient = useQueryClient() - - return (data: Partial) => { - queryClient.setQueryData( - createProfileQueryKey({did: data.did}), - oldData => { - if (!oldData) return oldData - return {...oldData, ...data} - }, - ) - } -} -``` - -**Stale Time Constants** (from `src/state/queries/index.ts`): - -```tsx -STALE.SECONDS.FIFTEEN // 15 seconds -STALE.MINUTES.ONE // 1 minute -STALE.MINUTES.FIVE // 5 minutes -STALE.HOURS.ONE // 1 hour -STALE.INFINITY // Never stale -``` - -**Paginated APIs:** Many atproto APIs return paginated results with a `cursor`. Use `useInfiniteQuery` for these: - -```tsx -export function useDraftsQuery() { - const agent = useAgent() - - return useInfiniteQuery({ - queryKey: createQueryKey('drafts'), - queryFn: async ({pageParam}) => { - const res = await agent.app.bsky.draft.getDrafts({cursor: pageParam}) - return res.data - }, - initialPageParam: undefined as string | undefined, - getNextPageParam: page => page.cursor, - }) -} -``` - -To get all items from pages: `data?.pages.flatMap(page => page.items) ?? []` - -**Persisted Queries** - -To persist query data across app restarts, `createQueryKey` supports a third -parameter called `options`, which has a `persistedVersion` property. When this -property is set to a number, the query will be persisted. - -When this property is updated (e.g. incremented), the persisted data will be cleared and replaced with the new data from the query function. This is useful for cases where the shape of the data has changed and old persisted data would no longer be valid. - -```tsx -export const createProfileQueryKey = (args: {did: string}) => - createQueryKey(profileQueryKeyRoot, args, {persistedVersion: 1}) -``` +- Build query keys with `createQueryKey(root, args)` (from `#/state/queries/util`) + using an object for `args`. The key root variable should match the hook name. +- Naming conventions: `use[Name]Query` for queries, `use[Name]Mutation` for + mutations, `use[Name]CacheMutation` for helpers that mutate cached data directly. +- Stale times come from `STALE` in `src/state/queries/index.ts`: `STALE.SECONDS.FIFTEEN`, + `STALE.MINUTES.ONE`, `STALE.MINUTES.FIVE`, `STALE.HOURS.ONE`, `STALE.INFINITY`. +- Paginated atproto APIs (those returning a `cursor`) use `useInfiniteQuery` with + `getNextPageParam: page => page.cursor`; flatten results with + `data?.pages.flatMap(page => page.items) ?? []`. +- Persist a query across restarts by passing options: + `createQueryKey(root, args, {persistedVersion: n})`. Bumping `n` clears the old + persisted data and refetches - do this whenever the data shape changes. +- Error handling in mutations: don't log network errors (just inform the user), + handle typed XRPC errors specifically (e.g. `err instanceof SomeNsid.SomeError`), + and send unexpected errors to `logger.error('...', {safeMessage: error})`. ### Preferences (React Context) -```tsx -// Simple boolean preference pattern -import {useAutoplayDisabled, useSetAutoplayDisabled} from '#/state/preferences' - -function SettingsScreen() { - const autoplayDisabled = useAutoplayDisabled() - const setAutoplayDisabled = useSetAutoplayDisabled() - - return -} -``` +Boolean/simple UI preferences are exposed as paired hooks from `#/state/preferences`, +e.g. `useAutoplayDisabled()` / `useSetAutoplayDisabled()`. ### Session State -```tsx -import {useSession, useAgent} from '#/state/session' - -function MyComponent() { - const {hasSession, currentAccount} = useSession() - const agent = useAgent() - - if (!hasSession) { - return - } - - // Use agent for API calls - const response = await agent.getProfile({actor: currentAccount.did}) -} -``` +`import {useSession, useAgent} from '#/state/session'`. `useSession()` gives +`hasSession` and `currentAccount`; `useAgent()` gives the atproto agent for API calls. ## Navigation -Navigation uses React Navigation with type-safe route parameters. - -```tsx -// Screen component -import {type NativeStackScreenProps} from '@react-navigation/native-stack' -import {type CommonNavigatorParams} from '#/lib/routes/types' - -type Props = NativeStackScreenProps - -export function ProfileScreen({route, navigation}: Props) { - const {name} = route.params // Type-safe params - - return {/* Screen content */} -} - -// Programmatic navigation -import {useNavigation} from '@react-navigation/native' - -const navigation = useNavigation() -navigation.navigate('Profile', {name: 'alice.bsky.social'}) - -// Or use the navigate helper -import {navigate} from '#/Navigation' -navigate('Profile', {name: 'alice.bsky.social'}) -``` +React Navigation with type-safe route params. Type a screen with +`NativeStackScreenProps` (`route`/`navigation` come +from props; params via `route.params`). Navigate programmatically with +`useNavigation()`, or the `navigate` helper from `#/Navigation`. Config lives in +`src/Navigation.tsx`, routes in `src/routes.ts`, types in `src/lib/routes/types.ts`. ## Platform-Specific Code -Use file extensions for platform-specific implementations: +Use file extensions for platform-specific implementations. The bundler resolves +them automatically - just import the base path normally, never a conditional +`require()`. ``` Component.tsx # Shared/default @@ -753,12 +414,11 @@ Component.ios.tsx # iOS-only Component.android.tsx # Android-only ``` -Example from Dialog: - -- `src/components/Dialog/index.tsx` – Native (uses BottomSheet) -- `src/components/Dialog/index.web.tsx` – Web (uses modal with Radix primitives) - -**Important:** The bundler automatically resolves platform-specific files. Just import normally: +Prefer grouping variants into a `Component/` directory (`index.tsx`, +`index.web.tsx`, `index.native.tsx`) rather than sibling `Component.web.tsx` files, +so the shared surface reads as one "macro" module (e.g. `src/components/Dialog/index.tsx` +native vs `index.web.tsx` web). The app has both patterns; the directory form is +preferred for new code. ```tsx // CORRECT - bundler picks storage.ts or storage.web.ts automatically @@ -770,15 +430,7 @@ const storage = IS_NATIVE : require('#/state/drafts/storage.web') ``` -Platform detection (for runtime logic, not imports): - -```tsx -import {IS_WEB, IS_NATIVE, IS_IOS, IS_ANDROID} from '#/env' - -if (IS_NATIVE) { - // Native-specific logic -} -``` +Runtime platform detection (not for imports): `import {IS_WEB, IS_NATIVE, IS_IOS, IS_ANDROID} from '#/env'`. ## Import Aliases From 3499d3646eb9836a77078485136c57d453b91836 Mon Sep 17 00:00:00 2001 From: DS Boyce <260543580+ds-boyce@users.noreply.github.com> Date: Thu, 9 Jul 2026 15:08:20 -0700 Subject: [PATCH 07/52] Migrate all search to v2 and remove v1 (#11125) --- eslint-suppressions.json | 16 -- src/analytics/features/types.ts | 2 - src/screens/Hashtag.tsx | 49 ++-- src/screens/Search/SearchResults.tsx | 83 ++----- src/screens/Search/Shell.tsx | 41 +--- .../Search/__tests__/searchParams.test.ts | 29 --- .../components/SearchLanguageDropdown.tsx | 46 +--- src/screens/Search/searchParams.ts | 21 -- src/screens/Search/utils.ts | 12 - src/screens/Topic.tsx | 27 ++- src/state/cache/post-shadow.ts | 2 +- src/state/queries/search-posts-v2.ts | 50 ++++- src/state/queries/search-posts.ts | 209 ------------------ src/state/queries/usePostThread/queryCache.ts | 2 +- 14 files changed, 114 insertions(+), 475 deletions(-) delete mode 100644 src/state/queries/search-posts.ts diff --git a/eslint-suppressions.json b/eslint-suppressions.json index c1d213d875..d3db4dfa15 100644 --- a/eslint-suppressions.json +++ b/eslint-suppressions.json @@ -1087,14 +1087,6 @@ "count": 1 } }, - "src/screens/Hashtag.tsx": { - "@typescript-eslint/no-floating-promises": { - "count": 2 - }, - "@typescript-eslint/no-misused-promises": { - "count": 1 - } - }, "src/screens/Home/NoFeedsPinned.tsx": { "@typescript-eslint/no-misused-promises": { "count": 1 @@ -1489,14 +1481,6 @@ "count": 1 } }, - "src/screens/Topic.tsx": { - "@typescript-eslint/no-floating-promises": { - "count": 2 - }, - "@typescript-eslint/no-misused-promises": { - "count": 1 - } - }, "src/state/a11y.tsx": { "@typescript-eslint/no-floating-promises": { "count": 1 diff --git a/src/analytics/features/types.ts b/src/analytics/features/types.ts index 4b2035fa53..63d1ab0da4 100644 --- a/src/analytics/features/types.ts +++ b/src/analytics/features/types.ts @@ -12,8 +12,6 @@ export enum Features { GroupChatsDisable = 'group_chats:disable', ComposerLanguageDetectionEnable = 'composer:language_detection:enable', PostGalleryEmbedEnable = 'post_gallery_embed:enable', - SearchV2Enable = 'search_v2:enable', - AdvancedSearchV2Enable = 'advanced_search_v2:enable', AATest = 'aa-test', } diff --git a/src/screens/Hashtag.tsx b/src/screens/Hashtag.tsx index e4f08291c9..39c2341f88 100644 --- a/src/screens/Hashtag.tsx +++ b/src/screens/Hashtag.tsx @@ -1,9 +1,7 @@ import {useCallback, useMemo, useState} from 'react' import {type ListRenderItemInfo, View} from 'react-native' import {type AppBskyFeedDefs} from '@atproto/api' -import {msg} from '@lingui/core/macro' -import {useLingui} from '@lingui/react' -import {Trans} from '@lingui/react/macro' +import {Trans, useLingui} from '@lingui/react/macro' import {type NativeStackScreenProps} from '@react-navigation/native-stack' import {HITSLOP_10} from '#/lib/constants' @@ -14,7 +12,7 @@ import {shareUrl} from '#/lib/sharing' import {cleanError} from '#/lib/strings/errors' import {sanitizeHandle} from '#/lib/strings/handles' import {enforceLen} from '#/lib/strings/helpers' -import {useSearchPostsQuery} from '#/state/queries/search-posts' +import {useSearchPostsV2Query} from '#/state/queries/search-posts-v2' import {useSession} from '#/state/session' import {useLoggedOutViewControls} from '#/state/shell/logged-out' import {useCloseAllActiveElements} from '#/state/util' @@ -43,7 +41,7 @@ export default function HashtagScreen({ route, }: NativeStackScreenProps) { const {tag, author} = route.params - const {_} = useLingui() + const {t: l} = useLingui() const decodedTag = useMemo(() => { return decodeURIComponent(tag) @@ -73,7 +71,7 @@ export default function HashtagScreen({ if (author) { url.searchParams.set('author', author) } - shareUrl(url.toString()) + void shareUrl(url.toString()) }, [tag, author]) const [activeTab, setActiveTab] = useState(0) @@ -85,7 +83,7 @@ export default function HashtagScreen({ const sections = useMemo(() => { return [ { - title: _(msg`Top`), + title: l`Top`, component: ( @@ -122,14 +120,14 @@ export default function HashtagScreen({ {author && ( {author.startsWith('did:') - ? _(msg`From ${sanitizedAuthor}`) - : _(msg`From @${sanitizedAuthor}`)} + ? l`From ${sanitizedAuthor}` + : l`From @${sanitizedAuthor}`} )} - - {!disableDetailsDialog && ( - + {cta} + +
)} - + ) } diff --git a/src/components/Post/Embed/index.tsx b/src/components/Post/Embed/index.tsx index fc65bea551..c715dbd3ad 100644 --- a/src/components/Post/Embed/index.tsx +++ b/src/components/Post/Embed/index.tsx @@ -321,7 +321,11 @@ export function QuoteEmbed({ linkDisabled /> {moderation ? ( - + ) : null} {richText ? ( void +}) { + const {_} = useLingui() + const {labeler, strings} = useLabelInfo(label) + const {gtMobile} = useBreakpoints() + const [details, setDetails] = useState('') + const {subject} = useLabelSubject({label}) + const isAccountReport = 'did' in subject + const agent = useAgent() + const sourceName = labeler + ? sanitizeHandle(labeler.creator.handle, '@') + : label.src + const [error, setError] = useState(null) + + const {mutate, isPending} = useMutation({ + mutationFn: async () => { + const $type = !isAccountReport + ? 'com.atproto.repo.strongRef' + : 'com.atproto.admin.defs#repoRef' + await agent.createModerationReport( + { + reasonType: ToolsOzoneReportDefs.REASONAPPEAL, + subject: { + $type, + ...subject, + }, + reason: details, + }, + { + encoding: 'application/json', + headers: { + 'atproto-proxy': `${label.src}#atproto_labeler`, + }, + }, + ) + }, + onError: err => { + if (err instanceof XRPCError && err.error === 'AlreadyAppealed') { + setError( + _( + msg`You've already appealed this label and it's being reviewed by our moderation team.`, + ), + ) + } else { + setError(_(msg`Failed to submit appeal, please try again.`)) + } + logger.error('Failed to submit label appeal', {message: err}) + }, + onSuccess: () => { + control.close() + Toast.show(_(msg({message: 'Appeal submitted', context: 'toast'}))) + }, + }) + + const onSubmit = () => mutate() + + return ( + <> + + + Appeal "{strings.name}" label + + + + This appeal will be sent to{' '} + control.close()} + style={[a.text_md, a.leading_snug]}> + {sourceName} + + . + + + + {error && ( + + {error} + + )} + + + + + + + + + {IS_ANDROID && } + + ) +} diff --git a/src/components/moderation/LabelsOnMe.tsx b/src/components/moderation/LabelsOnMe.tsx index 126e496887..85cb1e4a3a 100644 --- a/src/components/moderation/LabelsOnMe.tsx +++ b/src/components/moderation/LabelsOnMe.tsx @@ -1,9 +1,10 @@ import {type StyleProp, View, type ViewStyle} from 'react-native' -import {type AppBskyFeedDefs, type ComAtprotoLabelDefs} from '@atproto/api' +import {type ComAtprotoLabelDefs} from '@atproto/api' import {msg} from '@lingui/core/macro' import {useLingui} from '@lingui/react' import {Plural} from '@lingui/react/macro' +import {filterUserFacingLabels} from '#/lib/moderation' import {useSession} from '#/state/session' import {atoms as a} from '#/alf' import { @@ -19,12 +20,10 @@ import { } from '#/components/moderation/LabelsOnMeDialog' export function LabelsOnMe({ - type, labels, size, style, }: { - type: 'account' | 'content' labels: ComAtprotoLabelDefs.Label[] | undefined size?: ButtonSize style?: StyleProp @@ -36,18 +35,14 @@ export function LabelsOnMe({ if (!labels || !currentAccount) { return null } - labels = labels.filter( - l => - !l.val.startsWith('!') && - !(l.val === 'bot' && l.src === currentAccount.did), - ) + labels = filterUserFacingLabels(labels, currentAccount.did) if (!labels.length) { return null } return ( - + ) } - -export function LabelsOnMyPost({ - post, - style, -}: { - post: AppBskyFeedDefs.PostView - style?: StyleProp -}) { - const {currentAccount} = useSession() - if (post.author.did !== currentAccount?.did) { - return null - } - return ( - - ) -} diff --git a/src/components/moderation/LabelsOnMeDialog.tsx b/src/components/moderation/LabelsOnMeDialog.tsx index 83fe2e4019..963e4b762a 100644 --- a/src/components/moderation/LabelsOnMeDialog.tsx +++ b/src/components/moderation/LabelsOnMeDialog.tsx @@ -1,35 +1,34 @@ -import {useCallback, useMemo, useState} from 'react' +import {useMemo, useState} from 'react' import {View} from 'react-native' -import {type ComAtprotoLabelDefs, ToolsOzoneReportDefs} from '@atproto/api' -import {XRPCError} from '@atproto/api' +import {type ComAtprotoLabelDefs} from '@atproto/api' import {msg} from '@lingui/core/macro' import {useLingui} from '@lingui/react' import {Trans} from '@lingui/react/macro' -import {useMutation} from '@tanstack/react-query' import {useGetTimeAgo} from '#/lib/hooks/useTimeAgo' -import {useLabelSubject} from '#/lib/moderation' import {useLabelInfo} from '#/lib/moderation/useLabelInfo' import {makeProfileLink} from '#/lib/routes/links' import {sanitizeHandle} from '#/lib/strings/handles' -import {logger} from '#/logger' -import {useAgent, useSession} from '#/state/session' -import {atoms as a, useBreakpoints, useTheme} from '#/alf' -import {Button, ButtonIcon, ButtonText} from '#/components/Button' +import {useSession} from '#/state/session' +import {atoms as a, useTheme} from '#/alf' +import {Admonition} from '#/components/Admonition' +import {Button, ButtonText} from '#/components/Button' import * as Dialog from '#/components/Dialog' import {InlineLinkText} from '#/components/Link' -import * as Toast from '#/components/Toast' +import {AppealForm} from '#/components/moderation/AppealForm' import {Text} from '#/components/Typography' -import {IS_ANDROID} from '#/env' -import {Admonition} from '../Admonition' import {Divider} from '../Divider' -import {Loader} from '../Loader' export {useDialogControl as useLabelsOnMeDialogControl} from '#/components/Dialog' export interface LabelsOnMeDialogProps { control: Dialog.DialogOuterProps['control'] labels: ComAtprotoLabelDefs.Label[] + /** + * Whether the labels are being shown on the user's account or on a post. + * With `content`, the list may include account-level labels, which get a + * per-label callout. + */ type: 'account' | 'content' } @@ -62,7 +61,7 @@ function LabelsOnMeDialogInner(props: LabelsOnMeDialogProps) { label={ isAccount ? _(msg`The following labels were applied to your account.`) - : _(msg`The following labels were applied to your content.`) + : _(msg`The following labels were applied to this post.`) }> {appealingLabel ? ( Labels on your account ) : ( - Labels on your content + Labels applied to this post )} @@ -96,9 +95,10 @@ function LabelsOnMeDialogInner(props: LabelsOnMeDialogProps) { {labels.map(label => ( {!isSelfLabel && ( - - - {IS_ANDROID && } - - ) -} diff --git a/src/components/moderation/ModerationDetailsDialog.tsx b/src/components/moderation/ModerationDetailsDialog.tsx index ddc42effa3..bf19dc0363 100644 --- a/src/components/moderation/ModerationDetailsDialog.tsx +++ b/src/components/moderation/ModerationDetailsDialog.tsx @@ -1,3 +1,4 @@ +import {useState} from 'react' import {View} from 'react-native' import {type ModerationCause} from '@atproto/api' import {msg} from '@lingui/core/macro' @@ -9,10 +10,12 @@ import {useModerationCauseDescription} from '#/lib/moderation/useModerationCause import {makeProfileLink} from '#/lib/routes/links' import {listUriToHref} from '#/lib/strings/url-helpers' import {useSession} from '#/state/session' -import {atoms as a, useGutters, useTheme, web} from '#/alf' +import {atoms as a, useBreakpoints, useGutters, useTheme, web} from '#/alf' import {Admonition} from '#/components/Admonition' +import {Button, ButtonText} from '#/components/Button' import * as Dialog from '#/components/Dialog' import {InlineLinkText} from '#/components/Link' +import {AppealForm} from '#/components/moderation/AppealForm' import {type AppModerationCause} from '#/components/Pills' import {Text} from '#/components/Typography' import {IS_NATIVE} from '#/env' @@ -47,6 +50,19 @@ function ModerationDetailsDialogInner({ const desc = useModerationCauseDescription(modcause) const {currentAccount} = useSession() const timeDiff = useGetTimeAgo({future: true}) + const [isAppealing, setIsAppealing] = useState(false) + const {gtPhone} = useBreakpoints() + + /* + * Appeal eligibility: only for label causes on content belonging to the + * current user, where the label was not self-applied. + */ + const canAppeal = + modcause?.type === 'label' && + !!currentAccount && + modcause.label.src !== currentAccount.did && + (modcause.label.uri === currentAccount.did || + modcause.label.uri.startsWith(`at://${currentAccount.did}/`)) let name let description @@ -137,6 +153,23 @@ function ModerationDetailsDialogInner({ const sourceName = desc.source || desc.sourceDisplayName || _(msg`an unknown labeler`) + if (isAppealing && modcause?.type === 'label') { + return ( + + setIsAppealing(false)} + /> + + + ) + } + return ( + {canAppeal && ( + + + + You may appeal these labels if you feel they were placed in + error. + + + + + )} + {desc.isSubjectAccount && ( - This moderation was applied to the entire user account and will - appear on all posts. + This label was applied to the entire user account and will appear + on all posts. )} diff --git a/src/components/moderation/PostAlerts.tsx b/src/components/moderation/PostAlerts.tsx index 7a9c38cc61..4379c76cb3 100644 --- a/src/components/moderation/PostAlerts.tsx +++ b/src/components/moderation/PostAlerts.tsx @@ -1,28 +1,92 @@ -import {type StyleProp, type ViewStyle} from 'react-native' -import {type ModerationCause, type ModerationUI} from '@atproto/api' +import {type StyleProp, View, type ViewStyle} from 'react-native' +import { + type AppBskyFeedDefs, + type ComAtprotoLabelDefs, + type ModerationCause, + type ModerationUI, +} from '@atproto/api' +import {plural} from '@lingui/core/macro' +import {useLingui} from '@lingui/react/macro' -import {getModerationCauseKey, unique} from '#/lib/moderation' +import { + filterUserFacingLabels, + getModerationCauseKey, + unique, +} from '#/lib/moderation' +import {useSession} from '#/state/session' +import {atoms as a} from '#/alf' +import { + LabelsOnMeDialog, + useLabelsOnMeDialogControl, +} from '#/components/moderation/LabelsOnMeDialog' import * as Pills from '#/components/Pills' export function PostAlerts({ + post, modui, - size = 'sm', + view = 'compact', style, additionalCauses, }: { + post?: AppBskyFeedDefs.PostView modui: ModerationUI - size?: Pills.CommonProps['size'] + /** + * Expanded views (e.g. the thread anchor post) render larger pills and + * surface the "+n" additional labels pill. Compact views (feeds, replies) + * keep the alerts minimal. + */ + view?: 'expanded' | 'compact' includeMute?: boolean style?: StyleProp additionalCauses?: ModerationCause[] | Pills.AppModerationCause[] }) { - if (!modui.alert && !modui.inform && !additionalCauses?.length) { + const {currentAccount} = useSession() + const size: Pills.CommonProps['size'] = view === 'expanded' ? 'lg' : 'sm' + + const alerts = modui.alerts.filter(unique) + const informs = modui.informs.filter(unique) + /* + * The "+n" pill surfaces labels for the author to review and appeal, so it + * only applies when the viewer is the author, and only in expanded views. + * It renders even when no other moderation is visible, since it may be the + * author's only entry point to appeal labels on their content. + */ + const isOwnPost = !!post && post.author.did === currentAccount?.did + const allLabels: ComAtprotoLabelDefs.Label[] = + isOwnPost && view === 'expanded' + ? [...(post.labels ?? []), ...(post.author.labels ?? [])] + : [] + /* + * Labels that the moderation system already surfaces in this context - + * whether as an alert, an inform, or a blur handled by ContentHider - should + * not be repeated in the "+n" pill. + */ + const shownCauses = [...alerts, ...informs, ...modui.blurs] + const additionalLabels = filterUserFacingLabels( + allLabels, + currentAccount?.did, + ).filter(label => + shownCauses.every( + cause => + cause.type !== 'label' || + cause.label.val !== label.val || + cause.label.src !== label.src || + cause.label.uri !== label.uri, + ), + ) + + if ( + !modui.alert && + !modui.inform && + !additionalCauses?.length && + !additionalLabels.length + ) { return null } return ( - {modui.alerts.filter(unique).map(cause => ( + {alerts.map(cause => ( ))} - {modui.informs.filter(unique).map(cause => ( + {informs.map(cause => ( ))} + {additionalLabels.length ? ( + 0 || + informs.length > 0 || + (additionalCauses?.length ?? 0) > 0 + } + /> + ) : null} ) } + +function AdditionalLabels({ + labels, + size, + hasPrecedingPills, +}: { + labels: ComAtprotoLabelDefs.Label[] + size?: Pills.CommonProps['size'] + /** + * The compact "+n" syntax only makes sense as a continuation of other + * pills. When this pill stands alone, spell it out. + */ + hasPrecedingPills: boolean +}) { + const {t: l} = useLingui() + const control = useLabelsOnMeDialogControl() + + return ( + + + + { + control.open() + }} + /> + + ) +} diff --git a/src/lib/moderation.ts b/src/lib/moderation.ts index 2fb37009e2..0d2e3c4dbd 100644 --- a/src/lib/moderation.ts +++ b/src/lib/moderation.ts @@ -58,6 +58,21 @@ export function labelIsHideableOffense( return ['!hide', '!takedown'].includes(label.val) } +/** + * Filters out labels that are not user-facing: system labels (val prefixed + * with `!`) and the user's own "bot" self-label. + */ +export function filterUserFacingLabels( + labels: ComAtprotoLabelDefs.Label[], + currentAccountDid: string | undefined, +): ComAtprotoLabelDefs.Label[] { + return labels.filter( + label => + !label.val.startsWith('!') && + !(label.val === 'bot' && label.src === currentAccountDid), + ) +} + export function getLabelingServiceTitle({ displayName, handle, diff --git a/src/screens/Messages/components/ChatListItem.tsx b/src/screens/Messages/components/ChatListItem.tsx index d8de21209d..0acd0c28e7 100644 --- a/src/screens/Messages/components/ChatListItem.tsx +++ b/src/screens/Messages/components/ChatListItem.tsx @@ -164,7 +164,6 @@ function DirectChatItem({ isWithinLeftPanel ? null : ( ) diff --git a/src/screens/Messages/components/MessageInputEmbed.tsx b/src/screens/Messages/components/MessageInputEmbed.tsx index 4b69e1f67d..477939cd3e 100644 --- a/src/screens/Messages/components/MessageInputEmbed.tsx +++ b/src/screens/Messages/components/MessageInputEmbed.tsx @@ -240,9 +240,9 @@ function MessageInputPostEmbed({ {rt.text && ( - - - )} - diff --git a/src/view/com/posts/PostFeedItem.tsx b/src/view/com/posts/PostFeedItem.tsx index c0ff26206d..9129c4167f 100644 --- a/src/view/com/posts/PostFeedItem.tsx +++ b/src/view/com/posts/PostFeedItem.tsx @@ -39,7 +39,6 @@ import { maybeApplyGalleryOffsetStyles, } from '#/components/images/Gallery' import {ContentHider} from '#/components/moderation/ContentHider' -import {LabelsOnMyPost} from '#/components/moderation/LabelsOnMe' import {PostAlerts} from '#/components/moderation/PostAlerts' import {type AppModerationCause} from '#/components/Pills' import {Embed} from '#/components/Post/Embed' @@ -420,7 +419,6 @@ let FeedItemInner = ({ isParentNotFound={isParentNotFound} /> )} - Date: Sat, 11 Jul 2026 03:10:36 +0000 Subject: [PATCH 16/52] Nightly source-language update --- src/locale/locales/en/messages.po | 290 ++++++++++++++++-------------- 1 file changed, 154 insertions(+), 136 deletions(-) diff --git a/src/locale/locales/en/messages.po b/src/locale/locales/en/messages.po index cde488e472..2bc4a249ce 100644 --- a/src/locale/locales/en/messages.po +++ b/src/locale/locales/en/messages.po @@ -19,7 +19,7 @@ msgid "\"{interestsDisplayName}\" category (active)" msgstr "" #. A reply summary in chat -#: src/components/dms/MessageItem.tsx:882 +#: src/components/dms/MessageItem.tsx:901 msgid "(blocked message hidden)" msgstr "(blocked message hidden)" @@ -34,7 +34,7 @@ msgid "(contains embedded content)" msgstr "" #. A reply summary in chat -#: src/components/dms/MessageItem.tsx:896 +#: src/components/dms/MessageItem.tsx:915 msgid "(deleted message)" msgstr "(deleted message)" @@ -49,7 +49,7 @@ msgid "(invalid chat invite link)" msgstr "(invalid chat invite link)" #. A reply summary in chat -#: src/components/dms/MessageItem.tsx:890 +#: src/components/dms/MessageItem.tsx:909 msgid "(message sent before you joined)" msgstr "(message sent before you joined)" @@ -68,15 +68,10 @@ msgid "(quoted post)" msgstr "(quoted post)" #. placeholder {0}: labels.length -#: src/components/moderation/LabelsOnMe.tsx:63 +#: src/components/moderation/LabelsOnMe.tsx:57 msgid "{0, plural, one {# account label} other {# account labels}}" msgstr "{0, plural, one {# account label} other {# account labels}}" -#. placeholder {0}: labels.length -#: src/components/moderation/LabelsOnMe.tsx:69 -msgid "{0, plural, one {# content label} other {# content labels}}" -msgstr "{0, plural, one {# content label} other {# content labels}}" - #. placeholder {0}: diff.value #: src/lib/hooks/useTimeAgo.ts:169 msgid "{0, plural, one {# day} other {# days}}" @@ -102,6 +97,16 @@ msgstr "" msgid "{0, plural, one {# hour} other {# hours}}" msgstr "" +#. placeholder {0}: labels.length +#: src/components/moderation/PostAlerts.tsx:149 +msgid "{0, plural, one {# label applied to your post} other {# labels applied to your post}}" +msgstr "{0, plural, one {# label applied to your post} other {# labels applied to your post}}" + +#. placeholder {0}: labels.length +#: src/components/moderation/PostAlerts.tsx:156 +msgid "{0, plural, one {# label applied} other {# labels applied}}" +msgstr "{0, plural, one {# label applied} other {# labels applied}}" + #. placeholder {0}: likeCount ?? 0 #: src/screens/Post/PostLikedBy.tsx:34 msgid "{0, plural, one {# like} other {# likes}}" @@ -123,13 +128,13 @@ msgid "{0, plural, one {# month} other {# months}}" msgstr "" #. placeholder {0}: convo.details.unreadJoinRequestCount -#: src/screens/Messages/components/ChatListItem.tsx:225 +#: src/screens/Messages/components/ChatListItem.tsx:224 msgid "{0, plural, one {# new join request} other {# new join requests}}" msgstr "{0, plural, one {# new join request} other {# new join requests}}" #. placeholder {0}: reactions.length #. placeholder {1}: groupedReactions.map(g => g.value).join(' ') -#: src/components/dms/MessageItem.tsx:351 +#: src/components/dms/MessageItem.tsx:370 msgid "{0, plural, one {# person} other {# people}} reacted – {1}" msgstr "{0, plural, one {# person} other {# people}} reacted – {1}" @@ -247,7 +252,7 @@ msgid "{0} following" msgstr "" #. placeholder {0}: sanitizeHandle(profile.handle, '@') -#: src/components/dms/MessageItem.tsx:704 +#: src/components/dms/MessageItem.tsx:723 msgid "{0} is blocking you" msgstr "{0} is blocking you" @@ -305,7 +310,7 @@ msgstr "" #. placeholder {0}: createSanitizedDisplayName(memberSender) #. placeholder {1}: reaction.value -#: src/components/dms/MessageItem.tsx:346 +#: src/components/dms/MessageItem.tsx:365 msgid "{0} reacted {1}" msgstr "" @@ -659,7 +664,7 @@ msgstr "" msgid "{JOIN_REQUESTS_THRESHOLD}+ new join requests" msgstr "{JOIN_REQUESTS_THRESHOLD}+ new join requests" -#: src/screens/Messages/components/ChatListItem.tsx:220 +#: src/screens/Messages/components/ChatListItem.tsx:219 msgctxt "Displayed when there are more than 20 requests to join a group chat" msgid "{JOIN_REQUESTS_THRESHOLD}+ new join requests" msgstr "{JOIN_REQUESTS_THRESHOLD}+ new join requests" @@ -733,15 +738,15 @@ msgstr "" msgid "{rank}." msgstr "" -#: src/components/dms/MessageItem.tsx:791 +#: src/components/dms/MessageItem.tsx:810 msgid "{replierDisplayName} replied" msgstr "{replierDisplayName} replied" -#: src/components/dms/MessageItem.tsx:790 +#: src/components/dms/MessageItem.tsx:809 msgid "{replierDisplayName} replied to {originalName}" msgstr "{replierDisplayName} replied to {originalName}" -#: src/components/dms/MessageItem.tsx:788 +#: src/components/dms/MessageItem.tsx:807 msgid "{replierDisplayName} replied to you" msgstr "{replierDisplayName} replied to you" @@ -774,7 +779,9 @@ msgid "{userName}'s verifications" msgstr "" #. Number of images beyond the first 3 +#. placeholder {0}: labels.length #. placeholder {0}: totalNumber - 3 +#: src/components/moderation/PostAlerts.tsx:155 #: src/view/com/composer/ComposerReplyTo.tsx:278 msgid "+{0}" msgstr "+{0}" @@ -815,28 +822,28 @@ msgstr "" #. Like count display, the <0> tags enclose the number of likes in bold (will never be 0) #. placeholder {0}: formatPostStatCount(post.likeCount) #. placeholder {1}: post.likeCount -#: src/screens/PostThread/components/ThreadItemAnchor.tsx:492 +#: src/screens/PostThread/components/ThreadItemAnchor.tsx:491 msgid "<0>{0} {1, plural, one {like} other {likes}}" msgstr "" #. Quote count display, the <0> tags enclose the number of quotes in bold (will never be 0) #. placeholder {0}: formatPostStatCount(post.quoteCount) #. placeholder {1}: post.quoteCount -#: src/screens/PostThread/components/ThreadItemAnchor.tsx:473 +#: src/screens/PostThread/components/ThreadItemAnchor.tsx:472 msgid "<0>{0} {1, plural, one {quote} other {quotes}}" msgstr "" #. Repost count display, the <0> tags enclose the number of reposts in bold (will never be 0) #. placeholder {0}: formatPostStatCount(post.repostCount) #. placeholder {1}: post.repostCount -#: src/screens/PostThread/components/ThreadItemAnchor.tsx:452 +#: src/screens/PostThread/components/ThreadItemAnchor.tsx:451 msgid "<0>{0} {1, plural, one {repost} other {reposts}}" msgstr "" #. Save count display, the <0> tags enclose the number of saves in bold (will never be 0) #. placeholder {0}: formatPostStatCount(post.bookmarkCount) #. placeholder {1}: post.bookmarkCount -#: src/screens/PostThread/components/ThreadItemAnchor.tsx:510 +#: src/screens/PostThread/components/ThreadItemAnchor.tsx:509 msgid "<0>{0} {1, plural, one {save} other {saves}}" msgstr "" @@ -937,7 +944,7 @@ msgstr "" msgid "A new form of verification" msgstr "" -#: src/components/dms/MessageItem.tsx:801 +#: src/components/dms/MessageItem.tsx:820 msgid "A reply to a message sent before you joined" msgstr "A reply to a message sent before you joined" @@ -1051,12 +1058,12 @@ msgctxt "toast" msgid "Account muted" msgstr "" -#: src/components/moderation/ModerationDetailsDialog.tsx:107 +#: src/components/moderation/ModerationDetailsDialog.tsx:123 #: src/lib/moderation/useModerationCauseDescription.ts:99 msgid "Account Muted" msgstr "" -#: src/components/moderation/ModerationDetailsDialog.tsx:93 +#: src/components/moderation/ModerationDetailsDialog.tsx:109 msgid "Account Muted by List" msgstr "" @@ -1663,7 +1670,7 @@ msgstr "" msgid "An mockup of a iPhone showing the Bluesky app open to the profile of a verified user with a blue checkmark next to their display name." msgstr "" -#: src/components/moderation/ModerationDetailsDialog.tsx:138 +#: src/components/moderation/ModerationDetailsDialog.tsx:154 #: src/lib/moderation/useModerationCauseDescription.ts:145 msgid "an unknown labeler" msgstr "" @@ -1778,21 +1785,26 @@ msgstr "" msgid "App Passwords" msgstr "" -#: src/components/moderation/LabelsOnMeDialog.tsx:155 -#: src/components/moderation/LabelsOnMeDialog.tsx:158 +#: src/components/moderation/LabelsOnMeDialog.tsx:169 +#: src/components/moderation/LabelsOnMeDialog.tsx:172 +#: src/components/moderation/ModerationDetailsDialog.tsx:223 msgid "Appeal" msgstr "" #. placeholder {0}: strings.name -#: src/components/moderation/LabelsOnMeDialog.tsx:282 +#: src/components/moderation/AppealForm.tsx:93 msgid "Appeal \"{0}\" label" msgstr "" +#: src/components/moderation/ModerationDetailsDialog.tsx:159 +msgid "Appeal label" +msgstr "Appeal label" + #: src/features/liveNow/components/GoLiveDisabledDialog.tsx:93 msgid "Appeal livestream suspension" msgstr "" -#: src/components/moderation/LabelsOnMeDialog.tsx:272 +#: src/components/moderation/AppealForm.tsx:83 #: src/features/liveNow/components/GoLiveDisabledDialog.tsx:83 #: src/screens/Messages/components/ChatDisabled.tsx:125 msgctxt "toast" @@ -1815,6 +1827,10 @@ msgstr "" msgid "Appeal this decision" msgstr "" +#: src/components/moderation/ModerationDetailsDialog.tsx:219 +msgid "Appeal this label" +msgstr "Appeal this label" + #: src/Navigation.tsx:396 #: src/screens/Settings/AppearanceSettings.tsx:73 #: src/screens/Settings/Settings.tsx:225 @@ -1833,12 +1849,12 @@ msgid "Apply Pull Request" msgstr "" #. placeholder {0}: niceDate(i18n, createdAt, 'medium') -#: src/screens/PostThread/components/ThreadItemAnchor.tsx:632 +#: src/screens/PostThread/components/ThreadItemAnchor.tsx:631 msgid "Archived from {0}" msgstr "" -#: src/screens/PostThread/components/ThreadItemAnchor.tsx:603 -#: src/screens/PostThread/components/ThreadItemAnchor.tsx:641 +#: src/screens/PostThread/components/ThreadItemAnchor.tsx:602 +#: src/screens/PostThread/components/ThreadItemAnchor.tsx:640 msgid "Archived post" msgstr "" @@ -1980,8 +1996,8 @@ msgstr "" #: src/components/dms/InitiateChatFlow.tsx:540 #: src/components/dms/InitiateChatFlow.tsx:758 #: src/components/dms/InitiateChatFlow.tsx:765 -#: src/components/moderation/LabelsOnMeDialog.tsx:334 -#: src/components/moderation/LabelsOnMeDialog.tsx:335 +#: src/components/moderation/AppealForm.tsx:145 +#: src/components/moderation/AppealForm.tsx:146 #: src/screens/Login/ChooseAccountForm.tsx:96 #: src/screens/Login/ChooseAccountForm.tsx:100 #: src/screens/Login/ForgotPasswordForm.tsx:119 @@ -2215,7 +2231,7 @@ msgstr "bloomscrolling booksky" msgid "Bluesky" msgstr "" -#: src/screens/PostThread/components/ThreadItemAnchor.tsx:657 +#: src/screens/PostThread/components/ThreadItemAnchor.tsx:656 msgid "Bluesky cannot confirm the authenticity of the claimed date." msgstr "" @@ -2867,7 +2883,7 @@ msgstr "Click here to view the invite link for this group chat" msgid "Click to open tag menu for {0}" msgstr "" -#: src/components/dms/MessageItem.tsx:623 +#: src/components/dms/MessageItem.tsx:642 msgid "Click to retry failed message" msgstr "" @@ -3174,7 +3190,7 @@ msgstr "" msgid "Content languages" msgstr "" -#: src/components/moderation/ModerationDetailsDialog.tsx:86 +#: src/components/moderation/ModerationDetailsDialog.tsx:102 #: src/lib/moderation/useModerationCauseDescription.ts:83 msgid "Content Not Available" msgstr "" @@ -3183,7 +3199,7 @@ msgstr "" msgid "Content promoting or depicting self-harm" msgstr "" -#: src/components/moderation/ModerationDetailsDialog.tsx:54 +#: src/components/moderation/ModerationDetailsDialog.tsx:70 #: src/components/moderation/ScreenHider.tsx:100 #: src/lib/moderation/useGlobalLabelStrings.ts:22 #: src/lib/moderation/useModerationCauseDescription.ts:46 @@ -3243,7 +3259,7 @@ msgstr "" msgid "Conversation" msgstr "" -#: src/screens/Messages/components/ChatListItem.tsx:322 +#: src/screens/Messages/components/ChatListItem.tsx:321 msgid "Conversation deleted" msgstr "" @@ -4113,7 +4129,7 @@ msgstr "" msgid "Done" msgstr "" -#: src/components/dms/MessageItem.tsx:521 +#: src/components/dms/MessageItem.tsx:540 msgid "Double tap or long press the message to add a reaction" msgstr "" @@ -4655,7 +4671,7 @@ msgstr "" msgid "Expands or collapses post text" msgstr "" -#: src/lib/api/index.ts:491 +#: src/lib/api/index.ts:490 msgid "Expected uri to resolve to a record" msgstr "" @@ -4674,8 +4690,8 @@ msgstr "" #. placeholder {0}: timeDiff(Date.now(), label.exp) #. placeholder {0}: timeDiff(Date.now(), modcause.label.exp) -#: src/components/moderation/LabelsOnMeDialog.tsx:204 -#: src/components/moderation/ModerationDetailsDialog.tsx:224 +#: src/components/moderation/LabelsOnMeDialog.tsx:218 +#: src/components/moderation/ModerationDetailsDialog.tsx:294 msgid "Expires in {0}" msgstr "" @@ -5019,7 +5035,7 @@ msgstr "" msgid "Failed to send report" msgstr "Failed to send report" -#: src/components/moderation/LabelsOnMeDialog.tsx:266 +#: src/components/moderation/AppealForm.tsx:77 #: src/features/liveNow/components/GoLiveDisabledDialog.tsx:77 #: src/screens/Messages/components/ChatDisabled.tsx:119 msgid "Failed to submit appeal, please try again." @@ -5474,7 +5490,7 @@ msgstr "" msgid "Following Feed Preferences" msgstr "" -#: src/components/Pills.tsx:175 +#: src/components/Pills.tsx:212 #: src/screens/Profile/Header/Handle.tsx:33 msgid "Follows you" msgstr "" @@ -5793,7 +5809,7 @@ msgstr "" msgid "Go to profile" msgstr "" -#: src/screens/Messages/components/ChatListItem.tsx:212 +#: src/screens/Messages/components/ChatListItem.tsx:211 msgid "Go to the group chat named \"{chatName}\"" msgstr "Go to the group chat named \"{chatName}\"" @@ -6729,18 +6745,18 @@ msgstr "" msgid "Labels added" msgstr "" +#: src/components/moderation/LabelsOnMeDialog.tsx:78 +msgid "Labels applied to this post" +msgstr "Labels applied to this post" + #: src/screens/Profile/Sections/Labels.tsx:194 msgid "Labels are annotations on users and content. They can be used to hide, warn, and categorize the network." msgstr "" -#: src/components/moderation/LabelsOnMeDialog.tsx:77 +#: src/components/moderation/LabelsOnMeDialog.tsx:76 msgid "Labels on your account" msgstr "" -#: src/components/moderation/LabelsOnMeDialog.tsx:79 -msgid "Labels on your content" -msgstr "" - #: src/screens/Search/components/AdvancedSearchDialog/index.tsx:357 msgid "Language" msgstr "Language" @@ -7012,7 +7028,7 @@ msgstr "" msgid "Likes of your reposts" msgstr "" -#: src/screens/PostThread/components/ThreadItemAnchor.tsx:488 +#: src/screens/PostThread/components/ThreadItemAnchor.tsx:487 msgid "Likes on this post" msgstr "" @@ -7405,7 +7421,7 @@ msgstr "" msgid "Message {displayName}" msgstr "Message {displayName}" -#: src/screens/Messages/components/ChatListItem.tsx:323 +#: src/screens/Messages/components/ChatListItem.tsx:322 msgid "Message deleted" msgstr "" @@ -7414,7 +7430,7 @@ msgctxt "toast" msgid "Message deleted" msgstr "" -#: src/components/dms/MessageItem.tsx:617 +#: src/components/dms/MessageItem.tsx:636 msgid "Message failed to send." msgstr "Message failed to send." @@ -7445,11 +7461,11 @@ msgstr "" msgid "Messages" msgstr "" -#: src/components/dms/MessageItem.tsx:716 +#: src/components/dms/MessageItem.tsx:735 msgid "Messages from this person are hidden while they are blocking you." msgstr "Messages from this person are hidden while they are blocking you." -#: src/components/dms/MessageItem.tsx:711 +#: src/components/dms/MessageItem.tsx:730 msgid "Messages from this person are hidden while you are blocking them." msgstr "Messages from this person are hidden while you are blocking them." @@ -7480,7 +7496,7 @@ msgstr "" msgid "Moderation and content filters" msgstr "Moderation and content filters" -#: src/components/moderation/ModerationDetailsDialog.tsx:142 +#: src/components/moderation/ModerationDetailsDialog.tsx:175 msgid "Moderation details" msgstr "" @@ -7531,7 +7547,7 @@ msgstr "" msgid "Moderation tools" msgstr "" -#: src/components/moderation/ModerationDetailsDialog.tsx:56 +#: src/components/moderation/ModerationDetailsDialog.tsx:72 #: src/lib/moderation/useModerationCauseDescription.ts:48 msgid "Moderator has chosen to set a general warning on the content." msgstr "" @@ -8013,7 +8029,7 @@ msgstr "" msgid "No media yet" msgstr "" -#: src/screens/Messages/components/ChatListItem.tsx:311 +#: src/screens/Messages/components/ChatListItem.tsx:310 msgid "No messages yet" msgstr "" @@ -8279,9 +8295,9 @@ msgstr "" #: src/components/BotAccountAlert.tsx:52 #: src/components/BotAccountAlert.tsx:57 #: src/components/dms/InitiateChatFlow.tsx:733 -#: src/components/dms/MessageItem.tsx:723 +#: src/components/dms/MessageItem.tsx:742 #: src/screens/Login/PasswordUpdatedForm.tsx:35 -#: src/screens/PostThread/components/ThreadItemAnchor.tsx:663 +#: src/screens/PostThread/components/ThreadItemAnchor.tsx:662 msgid "Okay" msgstr "" @@ -8435,8 +8451,8 @@ msgstr "Open chat" msgid "Open chat member options for {displayName}" msgstr "Open chat member options for {displayName}" -#: src/screens/Messages/components/ChatListItem.tsx:491 -#: src/screens/Messages/components/ChatListItem.tsx:495 +#: src/screens/Messages/components/ChatListItem.tsx:490 +#: src/screens/Messages/components/ChatListItem.tsx:494 msgid "Open conversation options" msgstr "" @@ -9075,7 +9091,7 @@ msgid "Please enter your username" msgstr "" #. placeholder {0}: labeler ? sanitizeHandle(labeler.creator.handle, '@') : label.src -#: src/components/moderation/LabelsOnMeDialog.tsx:309 +#: src/components/moderation/AppealForm.tsx:120 msgid "Please explain why you think this label was incorrectly applied by {0}" msgstr "" @@ -9189,23 +9205,23 @@ msgctxt "toast" msgid "Post deleted" msgstr "" -#: src/lib/api/index.ts:190 +#: src/lib/api/index.ts:189 msgid "Post failed to upload. Please check your Internet connection and try again." msgstr "" -#: src/screens/PostThread/components/ThreadItemAnchor.tsx:132 -#: src/screens/PostThread/components/ThreadItemPost.tsx:117 -#: src/screens/PostThread/components/ThreadItemTreePost.tsx:110 +#: src/screens/PostThread/components/ThreadItemAnchor.tsx:131 +#: src/screens/PostThread/components/ThreadItemPost.tsx:116 +#: src/screens/PostThread/components/ThreadItemTreePost.tsx:109 #: src/screens/VideoFeed/index.tsx:547 msgid "Post has been deleted" msgstr "" -#: src/components/moderation/ModerationDetailsDialog.tsx:111 +#: src/components/moderation/ModerationDetailsDialog.tsx:127 #: src/lib/moderation/useModerationCauseDescription.ts:107 msgid "Post Hidden by Muted Word" msgstr "" -#: src/components/moderation/ModerationDetailsDialog.tsx:114 +#: src/components/moderation/ModerationDetailsDialog.tsx:130 #: src/lib/moderation/useModerationCauseDescription.ts:116 msgid "Post Hidden by You" msgstr "" @@ -9357,7 +9373,7 @@ msgstr "" msgid "Processing video..." msgstr "" -#: src/lib/api/index.ts:63 +#: src/lib/api/index.ts:62 msgid "Processing..." msgstr "" @@ -9477,7 +9493,7 @@ msgstr "" msgid "Quotes" msgstr "" -#: src/screens/PostThread/components/ThreadItemAnchor.tsx:469 +#: src/screens/PostThread/components/ThreadItemAnchor.tsx:468 msgid "Quotes of this post" msgstr "" @@ -9876,15 +9892,15 @@ msgctxt "description" msgid "Replied to you" msgstr "" -#: src/components/dms/MessageItem.tsx:906 +#: src/components/dms/MessageItem.tsx:925 msgid "Replied-to message from {senderName}, tap to scroll to it" msgstr "Replied-to message from {senderName}, tap to scroll to it" -#: src/components/dms/MessageItem.tsx:904 +#: src/components/dms/MessageItem.tsx:923 msgid "Replied-to message was sent before you joined" msgstr "Replied-to message was sent before you joined" -#: src/components/dms/MessageItem.tsx:907 +#: src/components/dms/MessageItem.tsx:926 msgid "Replied-to message, tap to scroll to it" msgstr "Replied-to message, tap to scroll to it" @@ -9921,12 +9937,12 @@ msgstr "" msgid "Reply ({0, plural, one {# reply} other {# replies}})" msgstr "" -#: src/components/moderation/ModerationDetailsDialog.tsx:120 +#: src/components/moderation/ModerationDetailsDialog.tsx:136 #: src/lib/moderation/useModerationCauseDescription.ts:126 msgid "Reply Hidden by Thread Author" msgstr "" -#: src/components/moderation/ModerationDetailsDialog.tsx:119 +#: src/components/moderation/ModerationDetailsDialog.tsx:135 #: src/lib/moderation/useModerationCauseDescription.ts:125 msgid "Reply Hidden by You" msgstr "" @@ -10085,7 +10101,7 @@ msgstr "" msgid "Reposts" msgstr "" -#: src/screens/PostThread/components/ThreadItemAnchor.tsx:448 +#: src/screens/PostThread/components/ThreadItemAnchor.tsx:447 msgid "Reposts of this post" msgstr "" @@ -10424,7 +10440,7 @@ msgstr "" msgid "Scroll right" msgstr "" -#: src/components/dms/MessageItem.tsx:800 +#: src/components/dms/MessageItem.tsx:819 msgid "Scroll to the message this is replying to" msgstr "Scroll to the message this is replying to" @@ -11195,7 +11211,7 @@ msgstr "" msgid "Show when you’re live" msgstr "" -#: src/screens/PostThread/components/ThreadItemAnchor.tsx:604 +#: src/screens/PostThread/components/ThreadItemAnchor.tsx:603 msgid "Shows information about when this post was created" msgstr "" @@ -11402,7 +11418,7 @@ msgid "Someone left the group" msgstr "Someone left the group" #. placeholder {0}: reaction.value -#: src/components/dms/MessageItem.tsx:348 +#: src/components/dms/MessageItem.tsx:367 msgid "Someone reacted {0}" msgstr "" @@ -11411,7 +11427,7 @@ msgstr "" msgid "Someone reacted {0} to {target}" msgstr "Someone reacted {0} to {target}" -#: src/components/dms/MessageItem.tsx:793 +#: src/components/dms/MessageItem.tsx:812 msgid "Someone replied" msgstr "Someone replied" @@ -11491,8 +11507,8 @@ msgstr "" msgid "Sort replies to the same post by:" msgstr "" -#: src/components/moderation/LabelsOnMeDialog.tsx:183 -#: src/components/moderation/ModerationDetailsDialog.tsx:202 +#: src/components/moderation/LabelsOnMeDialog.tsx:197 +#: src/components/moderation/ModerationDetailsDialog.tsx:272 msgid "Source: <0>{sourceName}" msgstr "" @@ -11615,8 +11631,8 @@ msgstr "" #: src/components/ageAssurance/AgeAssuranceAppealDialog.tsx:122 #: src/components/ageAssurance/AgeAssuranceAppealDialog.tsx:128 -#: src/components/moderation/LabelsOnMeDialog.tsx:343 -#: src/components/moderation/LabelsOnMeDialog.tsx:344 +#: src/components/moderation/AppealForm.tsx:154 +#: src/components/moderation/AppealForm.tsx:155 #: src/components/SendErrorReportDialog.tsx:90 #: src/components/SendErrorReportDialog.tsx:95 #: src/features/liveNow/components/GoLiveDisabledDialog.tsx:139 @@ -11790,7 +11806,7 @@ msgstr "Take the conversation private." msgid "Tap below to allow Bluesky to access your GPS location. We will then use that data to more accurately determine the content and features available in your region." msgstr "" -#: src/components/dms/MessageItem.tsx:662 +#: src/components/dms/MessageItem.tsx:681 msgid "Tap for details" msgstr "Tap for details" @@ -11848,7 +11864,7 @@ msgstr "Tap to remove your {0} reaction" msgid "Tap to request access to join this group chat" msgstr "Tap to request access to join this group chat" -#: src/components/dms/MessageItem.tsx:627 +#: src/components/dms/MessageItem.tsx:646 msgid "Tap to retry" msgstr "Tap to retry" @@ -11861,7 +11877,7 @@ msgstr "Tap to show {0} reactions" msgid "Tap to show all reactions" msgstr "Tap to show all reactions" -#: src/components/dms/MessageItem.tsx:374 +#: src/components/dms/MessageItem.tsx:393 msgid "Tap to view reactions" msgstr "Tap to view reactions" @@ -11918,7 +11934,7 @@ msgstr "" msgid "Text & tags" msgstr "" -#: src/components/moderation/LabelsOnMeDialog.tsx:307 +#: src/components/moderation/AppealForm.tsx:118 #: src/features/liveNow/components/GoLiveDisabledDialog.tsx:122 #: src/screens/Messages/components/ChatDisabled.tsx:142 msgid "Text input field" @@ -11973,7 +11989,7 @@ msgstr "" msgid "The app will be restarted" msgstr "" -#: src/components/moderation/ModerationDetailsDialog.tsx:123 +#: src/components/moderation/ModerationDetailsDialog.tsx:139 #: src/lib/moderation/useModerationCauseDescription.ts:129 msgid "The author of this thread has hidden this reply." msgstr "" @@ -12011,11 +12027,11 @@ msgid "The feed has been replaced with Discover." msgstr "" #: src/components/moderation/LabelsOnMeDialog.tsx:64 -msgid "The following labels were applied to your account." -msgstr "" +msgid "The following labels were applied to this post." +msgstr "The following labels were applied to this post." -#: src/components/moderation/LabelsOnMeDialog.tsx:65 -msgid "The following labels were applied to your content." +#: src/components/moderation/LabelsOnMeDialog.tsx:63 +msgid "The following labels were applied to your account." msgstr "" #: src/features/liveNow/components/GoLiveDialog.tsx:168 @@ -12261,7 +12277,7 @@ msgstr "" msgid "This action can be undone at any time." msgstr "" -#: src/components/moderation/LabelsOnMeDialog.tsx:285 +#: src/components/moderation/AppealForm.tsx:96 msgid "This appeal will be sent to <0>{sourceName}." msgstr "" @@ -12288,7 +12304,7 @@ msgstr "This chat has ended" msgid "This chat is full" msgstr "This chat is full" -#: src/screens/Messages/components/ChatListItem.tsx:377 +#: src/screens/Messages/components/ChatListItem.tsx:376 #: src/screens/Messages/components/ChatLocked.tsx:69 msgid "This chat is locked" msgstr "This chat is locked" @@ -12323,7 +12339,7 @@ msgstr "" msgid "This content is hosted by {0}. Do you want to enable external media?" msgstr "" -#: src/components/moderation/ModerationDetailsDialog.tsx:88 +#: src/components/moderation/ModerationDetailsDialog.tsx:104 #: src/lib/moderation/useModerationCauseDescription.ts:85 msgid "This content is not available because one of the users involved has blocked the other." msgstr "" @@ -12424,14 +12440,19 @@ msgstr "" msgid "This label lets the world know that this account is automated. If turned on, this label appears next to the account's name on their profile and posts. It can be turned on or off at any time." msgstr "This label lets the world know that this account is automated. If turned on, this label appears next to the account's name on their profile and posts. It can be turned on or off at any time." -#: src/components/moderation/ModerationDetailsDialog.tsx:184 +#: src/components/moderation/ModerationDetailsDialog.tsx:254 msgid "This label was applied by the author." msgstr "" -#: src/components/moderation/LabelsOnMeDialog.tsx:170 +#: src/components/moderation/LabelsOnMeDialog.tsx:184 msgid "This label was applied by you." msgstr "" +#: src/components/moderation/LabelsOnMeDialog.tsx:156 +#: src/components/moderation/ModerationDetailsDialog.tsx:231 +msgid "This label was applied to the entire user account and will appear on all posts." +msgstr "This label was applied to the entire user account and will appear on all posts." + #: src/screens/Profile/Sections/Labels.tsx:218 msgid "This labeler hasn't declared what labels it publishes, and may not be active." msgstr "" @@ -12453,17 +12474,17 @@ msgstr "" msgid "This list is empty." msgstr "" -#: src/screens/Messages/components/ChatListItem.tsx:336 +#: src/screens/Messages/components/ChatListItem.tsx:335 msgid "This message is hidden" msgstr "This message is hidden" -#: src/components/dms/MessageItem.tsx:660 -#: src/components/dms/MessageItem.tsx:689 +#: src/components/dms/MessageItem.tsx:679 +#: src/components/dms/MessageItem.tsx:708 msgid "This message is hidden because this user is blocking you." msgstr "This message is hidden because this user is blocking you." -#: src/components/dms/MessageItem.tsx:659 -#: src/components/dms/MessageItem.tsx:685 +#: src/components/dms/MessageItem.tsx:678 +#: src/components/dms/MessageItem.tsx:704 msgid "This message is hidden because you are blocking this user." msgstr "This message is hidden because you are blocking this user." @@ -12471,17 +12492,13 @@ msgstr "This message is hidden because you are blocking this user." msgid "This moderation service is unavailable. See below for more details. If this issue persists, contact us." msgstr "" -#: src/components/moderation/ModerationDetailsDialog.tsx:161 -msgid "This moderation was applied to the entire user account and will appear on all posts." -msgstr "This moderation was applied to the entire user account and will appear on all posts." - #: src/components/dms/MessagesListBlockedFooter.tsx:84 msgid "This person is blocking you" msgstr "This person is blocking you" #. placeholder {0}: niceDate(i18n, createdAt) #. placeholder {1}: niceDate(i18n, indexedAt) -#: src/screens/PostThread/components/ThreadItemAnchor.tsx:644 +#: src/screens/PostThread/components/ThreadItemAnchor.tsx:643 msgid "This post claims to have been created on <0>{0}, but was first seen by Bluesky on <1>{1}." msgstr "" @@ -12546,7 +12563,7 @@ msgstr "" msgid "This user has blocked you and cannot be messaged." msgstr "This user has blocked you and cannot be messaged." -#: src/components/moderation/ModerationDetailsDialog.tsx:83 +#: src/components/moderation/ModerationDetailsDialog.tsx:99 #: src/lib/moderation/useModerationCauseDescription.ts:76 msgid "This user has blocked you. You cannot view their content." msgstr "" @@ -12560,12 +12577,12 @@ msgid "This user has requested that their content only be shown to signed-in use msgstr "" #. placeholder {0}: list.name -#: src/components/moderation/ModerationDetailsDialog.tsx:63 +#: src/components/moderation/ModerationDetailsDialog.tsx:79 msgid "This user is included in the <0>{0} list which you have blocked." msgstr "" #. placeholder {0}: list.name -#: src/components/moderation/ModerationDetailsDialog.tsx:95 +#: src/components/moderation/ModerationDetailsDialog.tsx:111 msgid "This user is included in the <0>{0} list which you have muted." msgstr "" @@ -12864,7 +12881,7 @@ msgstr "" msgid "Unavailable feed information" msgstr "" -#: src/components/dms/MessageItem.tsx:727 +#: src/components/dms/MessageItem.tsx:746 #: src/components/dms/MessagesListBlockedFooter.tsx:97 #: src/components/dms/MessagesListBlockedFooter.tsx:104 #: src/components/moderation/BlockDialog.tsx:186 @@ -13199,13 +13216,13 @@ msgstr "" msgid "Uploading GIF..." msgstr "" -#: src/lib/api/index.ts:328 -#: src/lib/api/index.ts:355 +#: src/lib/api/index.ts:327 +#: src/lib/api/index.ts:354 msgid "Uploading images..." msgstr "" -#: src/lib/api/index.ts:427 -#: src/lib/api/index.ts:451 +#: src/lib/api/index.ts:426 +#: src/lib/api/index.ts:450 msgid "Uploading link thumbnail..." msgstr "" @@ -13258,7 +13275,7 @@ msgctxt "toast" msgid "User blocked" msgstr "" -#: src/components/moderation/ModerationDetailsDialog.tsx:75 +#: src/components/moderation/ModerationDetailsDialog.tsx:91 #: src/lib/moderation/useModerationCauseDescription.ts:64 msgid "User Blocked" msgstr "" @@ -13272,7 +13289,7 @@ msgstr "" msgid "User blocked by list" msgstr "" -#: src/components/moderation/ModerationDetailsDialog.tsx:61 +#: src/components/moderation/ModerationDetailsDialog.tsx:77 msgid "User Blocked by List" msgstr "" @@ -13280,7 +13297,7 @@ msgstr "" msgid "User Blocking You" msgstr "" -#: src/components/moderation/ModerationDetailsDialog.tsx:81 +#: src/components/moderation/ModerationDetailsDialog.tsx:97 msgid "User Blocks You" msgstr "" @@ -13524,7 +13541,7 @@ msgid "View {0}" msgstr "View {0}" #. placeholder {0}: profile.handle -#: src/screens/Profile/Header/Shell.tsx:257 +#: src/screens/Profile/Header/Shell.tsx:256 msgid "View {0}'s avatar" msgstr "" @@ -13586,7 +13603,7 @@ msgstr "View incoming requests" msgid "View incoming requests to join this group chat" msgstr "View incoming requests to join this group chat" -#: src/components/moderation/LabelsOnMe.tsx:56 +#: src/components/moderation/LabelsOnMe.tsx:51 msgid "View information about these labels" msgstr "" @@ -14131,7 +14148,7 @@ msgid "You are accessing Bluesky from a region that legally requires us to verif msgstr "" #. placeholder {0}: sanitizeHandle(profile.handle, '@') -#: src/components/dms/MessageItem.tsx:700 +#: src/components/dms/MessageItem.tsx:719 msgid "You are blocking {0}" msgstr "You are blocking {0}" @@ -14316,7 +14333,7 @@ msgstr "" msgid "You have been previously removed from this group and can’t join it using this link." msgstr "You have been previously removed from this group and can’t join it using this link." -#: src/components/moderation/ModerationDetailsDialog.tsx:77 +#: src/components/moderation/ModerationDetailsDialog.tsx:93 #: src/lib/moderation/useModerationCauseDescription.ts:58 #: src/lib/moderation/useModerationCauseDescription.ts:66 msgid "You have blocked this user. You cannot view their content." @@ -14340,7 +14357,7 @@ msgstr "" msgid "You have hidden this post" msgstr "" -#: src/components/moderation/ModerationDetailsDialog.tsx:115 +#: src/components/moderation/ModerationDetailsDialog.tsx:131 msgid "You have hidden this post." msgstr "" @@ -14348,7 +14365,7 @@ msgstr "" msgid "You have marked this account as automated. You can remove it at any time from your account settings." msgstr "You have marked this account as automated. You can remove it at any time from your account settings." -#: src/components/moderation/ModerationDetailsDialog.tsx:108 +#: src/components/moderation/ModerationDetailsDialog.tsx:124 #: src/lib/moderation/useModerationCauseDescription.ts:100 msgid "You have muted this account." msgstr "" @@ -14410,7 +14427,7 @@ msgstr "" msgid "You haven't muted any words or tags yet" msgstr "" -#: src/components/moderation/ModerationDetailsDialog.tsx:122 +#: src/components/moderation/ModerationDetailsDialog.tsx:138 #: src/lib/moderation/useModerationCauseDescription.ts:128 msgid "You hid this reply." msgstr "" @@ -14428,11 +14445,12 @@ msgstr "" msgid "You joined Bluesky using a starter pack {timeAgoString} ago" msgstr "" -#: src/components/moderation/LabelsOnMeDialog.tsx:84 +#: src/components/moderation/LabelsOnMeDialog.tsx:83 msgid "You may appeal non-self labels if you feel they were placed in error." msgstr "" -#: src/components/moderation/LabelsOnMeDialog.tsx:89 +#: src/components/moderation/LabelsOnMeDialog.tsx:88 +#: src/components/moderation/ModerationDetailsDialog.tsx:210 msgid "You may appeal these labels if you feel they were placed in error." msgstr "" @@ -14483,7 +14501,7 @@ msgid "You probably want to restart the app now." msgstr "" #. placeholder {0}: reaction.value -#: src/components/dms/MessageItem.tsx:341 +#: src/components/dms/MessageItem.tsx:360 msgid "You reacted {0}" msgstr "" @@ -14497,15 +14515,15 @@ msgstr "You reacted {0} to {target}" msgid "You recently changed your birthdate" msgstr "" -#: src/components/dms/MessageItem.tsx:786 +#: src/components/dms/MessageItem.tsx:805 msgid "You replied" msgstr "You replied" -#: src/components/dms/MessageItem.tsx:785 +#: src/components/dms/MessageItem.tsx:804 msgid "You replied to {originalName}" msgstr "You replied to {originalName}" -#: src/components/dms/MessageItem.tsx:783 +#: src/components/dms/MessageItem.tsx:802 msgid "You replied to yourself" msgstr "You replied to yourself" @@ -14596,11 +14614,11 @@ msgstr "" msgid "You're signed in with an App Password. Please sign in with your main password to continue deactivating your account." msgstr "" -#: src/components/moderation/LabelsOnMeDialog.tsx:262 +#: src/components/moderation/AppealForm.tsx:73 msgid "You've already appealed this label and it's being reviewed by our moderation team." msgstr "" -#: src/components/moderation/ModerationDetailsDialog.tsx:112 +#: src/components/moderation/ModerationDetailsDialog.tsx:128 #: src/lib/moderation/useModerationCauseDescription.ts:109 msgid "You've chosen to hide a word or tag within this post." msgstr "" From 0701a08a6352556fe5bea8a7bf0790a08a01b367 Mon Sep 17 00:00:00 2001 From: DS Boyce <260543580+ds-boyce@users.noreply.github.com> Date: Mon, 13 Jul 2026 07:23:08 -0700 Subject: [PATCH 17/52] Ensure long trending topic titles can wrap (#11141) --- .../shell/desktop/SidebarTrendingTopics.tsx | 21 ++++++++----------- 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/src/view/shell/desktop/SidebarTrendingTopics.tsx b/src/view/shell/desktop/SidebarTrendingTopics.tsx index 3a054faa69..f337edb86a 100644 --- a/src/view/shell/desktop/SidebarTrendingTopics.tsx +++ b/src/view/shell/desktop/SidebarTrendingTopics.tsx @@ -1,7 +1,5 @@ import {View} from 'react-native' -import {msg} from '@lingui/core/macro' -import {useLingui} from '@lingui/react' -import {Trans} from '@lingui/react/macro' +import {Trans, useLingui} from '@lingui/react/macro' import { useTrendingSettings, @@ -28,7 +26,7 @@ export function SidebarTrendingTopics() { function Inner() { const t = useTheme() - const {_} = useLingui() + const {t: l} = useLingui() const ax = useAnalytics() const trendingPrompt = Prompt.usePromptControl() const {setTrendingDisabled} = useTrendingSettingsApi() @@ -54,7 +52,7 @@ function Inner() { size="tiny" color="secondary" shape="round" - label={_(msg`Trending options`)} + label={l`Trending options`} onPress={() => trendingPrompt.open()} style={[a.bg_transparent, {marginTop: -6, marginRight: -6}]}> @@ -90,12 +88,11 @@ function Inner() { { ax.metric('trendingTopic:click', {context: 'sidebar'}) }}> {({hovered}) => ( - + + ]}> {topic.displayName ?? topic.topic} @@ -126,9 +123,9 @@ function Inner() { From 04ebd02ce3dda50571827a91b491d38183ce235a Mon Sep 17 00:00:00 2001 From: Alex Benzer Date: Mon, 13 Jul 2026 12:55:15 -0700 Subject: [PATCH 18/52] Prototype: Show known likers on post thread page (#11080) Co-authored-by: Eric Bailey --- src/analytics/features/types.ts | 2 + src/analytics/metrics/types.ts | 2 + .../PostThread/components/LikesStat.tsx | 206 ++++++++++++++++++ .../components/ThreadItemAnchor.tsx | 25 +-- src/state/queries/post-liked-by.ts | 51 +++++ 5 files changed, 263 insertions(+), 23 deletions(-) create mode 100644 src/screens/PostThread/components/LikesStat.tsx diff --git a/src/analytics/features/types.ts b/src/analytics/features/types.ts index 63d1ab0da4..b890873a37 100644 --- a/src/analytics/features/types.ts +++ b/src/analytics/features/types.ts @@ -12,6 +12,8 @@ export enum Features { GroupChatsDisable = 'group_chats:disable', ComposerLanguageDetectionEnable = 'composer:language_detection:enable', PostGalleryEmbedEnable = 'post_gallery_embed:enable', + PostThreadKnownLikersEnable = 'post_thread:known_likers:enable', + PostThreadKnownLikersFetchEnable = 'post_thread:known_likers:fetch:enable', AATest = 'aa-test', } diff --git a/src/analytics/metrics/types.ts b/src/analytics/metrics/types.ts index b988595bb2..ef875ff5bf 100644 --- a/src/analytics/metrics/types.ts +++ b/src/analytics/metrics/types.ts @@ -1461,4 +1461,6 @@ export type Events = { jobId?: string elapsedInPhaseMs: number } + + 'post:likedBy:click': {} } diff --git a/src/screens/PostThread/components/LikesStat.tsx b/src/screens/PostThread/components/LikesStat.tsx new file mode 100644 index 0000000000..7b6182d3c0 --- /dev/null +++ b/src/screens/PostThread/components/LikesStat.tsx @@ -0,0 +1,206 @@ +import {View} from 'react-native' +import {type AppBskyFeedDefs, AtUri, moderateProfile} from '@atproto/api' +import {plural} from '@lingui/core/macro' +import {Plural, Trans, useLingui} from '@lingui/react/macro' + +import {makeProfileLink} from '#/lib/routes/links' +import {sanitizeDisplayName} from '#/lib/strings/display-names' +import {useModerationOpts} from '#/state/preferences/moderation-opts' +import {useLikedBySampleQuery} from '#/state/queries/post-liked-by' +import {useSession} from '#/state/session' +import {atoms as a, useTheme} from '#/alf' +import {AvatarStack} from '#/components/AvatarStack' +import {InlineLinkText, Link} from '#/components/Link' +import {useFormatPostStatCount} from '#/components/PostControls/util' +import {ProfileHoverCard} from '#/components/ProfileHoverCard' +import {Text} from '#/components/Typography' +import {useAnalytics} from '#/analytics' + +const AVI_SIZE = 20 + +/** + * The likes stat for the expanded anchor post. When the viewer follows some + * of the post's recent likers, renders social proof - a face pile plus + * "Liked by A, B, and N others" - in place of the plain "N likes" text, + * which it falls back to otherwise. + * + * Known likers are sourced client-side from a single `getLikes` request (100 + * likes, the API max per page), so they are a sample of the most recent + * likers, not an exhaustive list. Only the faces and names are affected by + * sampling - the "N others" count is derived from the post's total like + * count. + */ +export function LikesStat({post}: {post: AppBskyFeedDefs.PostView}) { + const t = useTheme() + const {t: l} = useLingui() + const {hasSession, currentAccount} = useSession() + const moderationOpts = useModerationOpts() + const formatPostStatCount = useFormatPostStatCount() + const ax = useAnalytics() + + const likeCount = post.likeCount ?? 0 + /* + * Kill switch for the getLikes sample request itself, separate from the + * display gate below. + */ + const fetchEnabled = ax.features.enabled( + ax.features.PostThreadKnownLikersFetchEnable, + ) + const {data} = useLikedBySampleQuery({ + uri: fetchEnabled && hasSession && likeCount > 0 ? post.uri : undefined, + }) + + if (likeCount === 0) return null + + const urip = new AtUri(post.uri) + const likesHref = makeProfileLink(post.author, 'post', urip.rkey, 'liked-by') + const onPressLikedBy = () => ax.metric('post:likedBy:click', {}) + + const knownLikersAndModeration = moderationOpts + ? (data?.likes ?? []) + .map(like => like.actor) + .map(actor => ({ + actor, + moderation: moderateProfile(actor, moderationOpts), + })) + .filter(({actor, moderation}) => { + const modui = moderation.ui('profileList') + const isMe = actor.did === currentAccount?.did + const following = actor.viewer?.following + return !isMe && following && !modui.filter + }) + : [] + + const showKnownLikers = + knownLikersAndModeration.length > 0 && + ax.features.enabled(ax.features.PostThreadKnownLikersEnable) + + if (!showKnownLikers) { + return ( + + + + + {formatPostStatCount(likeCount)} + {' '} + + + + + ) + } + + const aviStackProfiles = knownLikersAndModeration + .slice(0, 3) + .map(({actor}) => actor) + const names = knownLikersAndModeration + .slice(0, 2) + .map(({actor, moderation}) => { + return { + did: actor.did, + href: makeProfileLink(actor), + displayName: sanitizeDisplayName( + actor.displayName || actor.handle, + moderation.ui('displayName'), + ), + } + }) + const others = likeCount - names.length + + /* + * The row link's a11y label mirrors the visible sentence so screen readers + * announce the social proof. + */ + const othersLabel = plural(others, { + one: `${formatPostStatCount(others)} other`, + other: `${formatPostStatCount(others)} others`, + }) + const rowLabel = + names.length >= 2 + ? others > 0 + ? l`${names[0].displayName}, ${names[1].displayName}, and ${othersLabel} like this` + : l`${names[0].displayName} and ${names[1].displayName} like this` + : others > 0 + ? l`${names[0].displayName} and ${othersLabel} like this` + : l`${names[0].displayName} likes this` + + const textStyle = [a.text_md, t.atoms.text_contrast_medium] + const nameStyle = [a.text_md, a.font_semi_bold, t.atoms.text] + + /* + * Nested inside the row link, but the deepest link claims the press, so + * tapping a name goes to that profile while the rest of the row goes to + * the likes list. Same pattern as NotificationFeedItem's author links. + */ + const nameLink = (name: (typeof names)[number]) => ( + + + {name.displayName} + + + ) + + return ( + /* + * The full-width wrapper keeps the social proof on its own line within + * the wrapping stats row, rather than wrapping mid-row and orphaning + * whichever count stat comes last. The link itself hugs its content so + * the empty space to the right of the text is not pressable. + */ + + + + + {names.length >= 2 ? ( + others > 0 ? ( + + {nameLink(names[0])}, {nameLink(names[1])}, and{' '} + {' '} + like this + + ) : ( + + {nameLink(names[0])} and {nameLink(names[1])} like this + + ) + ) : others > 0 ? ( + + {nameLink(names[0])} and{' '} + {' '} + like this + + ) : ( + + {nameLink(names[0])} likes this + + )} + + + + ) +} diff --git a/src/screens/PostThread/components/ThreadItemAnchor.tsx b/src/screens/PostThread/components/ThreadItemAnchor.tsx index e7f3b5a1cb..f88ad000c1 100644 --- a/src/screens/PostThread/components/ThreadItemAnchor.tsx +++ b/src/screens/PostThread/components/ThreadItemAnchor.tsx @@ -28,6 +28,7 @@ import {type OnPostSuccessData} from '#/state/shell/composer' import {useMergedThreadgateHiddenReplies} from '#/state/threadgate-hidden-replies' import {type PostSource} from '#/state/unstable-post-source' import {PreviewableUserAvatar} from '#/view/com/util/UserAvatar' +import {LikesStat} from '#/screens/PostThread/components/LikesStat' import {ThreadItemAnchorFollowButton} from '#/screens/PostThread/components/ThreadItemAnchorFollowButton' import { LINEAR_AVI_WIDTH, @@ -199,10 +200,6 @@ const ThreadItemAnchorInner = memo(function ThreadItemAnchorInner({ const authorHref = makeProfileLink(post.author) const isThreadAuthor = getThreadAuthor(post, record) === currentAccount?.did - const likesHref = useMemo(() => { - const urip = new AtUri(post.uri) - return makeProfileLink(post.author, 'post', urip.rkey, 'liked-by') - }, [post.uri, post.author]) const repostsHref = useMemo(() => { const urip = new AtUri(post.uri) return makeProfileLink(post.author, 'post', urip.rkey, 'reposted-by') @@ -443,6 +440,7 @@ const ThreadItemAnchorInner = memo(function ThreadItemAnchorInner({ a.py_md, t.atoms.border_contrast_low, ]}> + {post.repostCount != null && post.repostCount !== 0 ? ( ) : null} - {post.likeCount != null && post.likeCount !== 0 ? ( - - - - - {formatPostStatCount(post.likeCount)} - {' '} - - - - - ) : null} {post.bookmarkCount != null && post.bookmarkCount !== 0 ? ( + createQueryKey(likedBySampleQueryKeyRoot, args) + +/** + * A single-request sample of a post's most recent likers, as many as the API + * allows in one page (100). Used for the known-likers social proof on the + * post thread page. Kept separate from `useLikedByQuery` so it does not + * perturb the liked-by screen's pagination. + */ +export function useLikedBySampleQuery({uri}: {uri: string | undefined}) { + const agent = useAgent() + return useQuery({ + queryKey: createLikedBySampleQueryKey({uri: uri ?? ''}), + queryFn: async () => { + const res = await agent.getLikes({uri: uri ?? '', limit: SAMPLE_SIZE}) + return res.data + }, + staleTime: STALE.MINUTES.FIVE, + enabled: !!uri, + /* + * Consumers fall back to a plain like count when this query fails, so + * failing fast is preferable to amplifying getLikes load with retries. + */ + retry: 1, + }) +} + export function* findAllProfilesInQueryData( queryClient: QueryClient, did: string, @@ -60,4 +97,18 @@ export function* findAllProfilesInQueryData( } } } + const sampleQueryDatas = + queryClient.getQueriesData({ + queryKey: [likedBySampleQueryKeyRoot], + }) + for (const [_queryKey, queryData] of sampleQueryDatas) { + if (!queryData?.likes) { + continue + } + for (const like of queryData.likes) { + if (like.actor.did === did) { + yield like.actor + } + } + } } From 19872124bfcca21af4b59026ff54adbf7325b6aa Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 13 Jul 2026 23:48:14 +0300 Subject: [PATCH 19/52] Bump the actions group with 6 updates (#11148) --- .github/workflows/build-and-push-bskyweb-aws.yaml | 8 ++++---- .github/workflows/build-and-push-bskyweb-ghcr.yaml | 8 ++++---- .github/workflows/build-and-push-embedr-aws.yaml | 8 ++++---- .github/workflows/build-and-push-link-aws.yaml | 8 ++++---- .github/workflows/build-and-push-ogcard-aws.yaml | 8 ++++---- .github/workflows/claude-mention.yml | 2 +- .github/workflows/claude-review.yml | 2 +- .github/workflows/pull-request-comment.yml | 6 +++--- .github/workflows/pull-request-commit.yml | 6 +++--- 9 files changed, 28 insertions(+), 28 deletions(-) diff --git a/.github/workflows/build-and-push-bskyweb-aws.yaml b/.github/workflows/build-and-push-bskyweb-aws.yaml index 52bb3cd18e..324ce2fc91 100644 --- a/.github/workflows/build-and-push-bskyweb-aws.yaml +++ b/.github/workflows/build-and-push-bskyweb-aws.yaml @@ -25,10 +25,10 @@ jobs: uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - name: Setup Docker buildx - uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0 + uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0 - name: Log into registry ${{ env.REGISTRY }} - uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0 + uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4.4.0 with: registry: ${{ env.REGISTRY }} username: ${{ env.USERNAME}} @@ -36,7 +36,7 @@ jobs: - name: Extract Docker metadata id: meta - uses: docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9 # v6.1.0 + uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6.2.0 with: images: | ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} @@ -51,7 +51,7 @@ jobs: - name: Build and push Docker image id: build-and-push - uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0 + uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0 with: context: . push: ${{ github.event_name != 'pull_request' }} diff --git a/.github/workflows/build-and-push-bskyweb-ghcr.yaml b/.github/workflows/build-and-push-bskyweb-ghcr.yaml index 4de9407866..7703297df9 100644 --- a/.github/workflows/build-and-push-bskyweb-ghcr.yaml +++ b/.github/workflows/build-and-push-bskyweb-ghcr.yaml @@ -26,10 +26,10 @@ jobs: uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - name: Setup Docker buildx - uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0 + uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0 - name: Log into registry ${{ env.REGISTRY }} - uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0 + uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4.4.0 with: registry: ${{ env.REGISTRY }} username: ${{ env.USERNAME }} @@ -37,7 +37,7 @@ jobs: - name: Extract Docker metadata id: meta - uses: docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9 # v6.1.0 + uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6.2.0 with: images: | ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} @@ -46,7 +46,7 @@ jobs: - name: Build and push Docker image id: build-and-push - uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0 + uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0 with: context: . push: ${{ github.event_name != 'pull_request' }} diff --git a/.github/workflows/build-and-push-embedr-aws.yaml b/.github/workflows/build-and-push-embedr-aws.yaml index b54920a208..7db93ecaad 100644 --- a/.github/workflows/build-and-push-embedr-aws.yaml +++ b/.github/workflows/build-and-push-embedr-aws.yaml @@ -25,10 +25,10 @@ jobs: uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - name: Setup Docker buildx - uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0 + uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0 - name: Log into registry ${{ env.REGISTRY }} - uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0 + uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4.4.0 with: registry: ${{ env.REGISTRY }} username: ${{ env.USERNAME}} @@ -36,7 +36,7 @@ jobs: - name: Extract Docker metadata id: meta - uses: docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9 # v6.1.0 + uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6.2.0 with: images: | ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} @@ -45,7 +45,7 @@ jobs: - name: Build and push Docker image id: build-and-push - uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0 + uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0 with: context: . push: ${{ github.event_name != 'pull_request' }} diff --git a/.github/workflows/build-and-push-link-aws.yaml b/.github/workflows/build-and-push-link-aws.yaml index a459021fae..766b25253d 100644 --- a/.github/workflows/build-and-push-link-aws.yaml +++ b/.github/workflows/build-and-push-link-aws.yaml @@ -25,10 +25,10 @@ jobs: uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - name: Setup Docker buildx - uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0 + uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0 - name: Log into registry ${{ env.REGISTRY }} - uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0 + uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4.4.0 with: registry: ${{ env.REGISTRY }} username: ${{ env.USERNAME}} @@ -36,7 +36,7 @@ jobs: - name: Extract Docker metadata id: meta - uses: docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9 # v6.1.0 + uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6.2.0 with: images: | ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} @@ -45,7 +45,7 @@ jobs: - name: Build and push Docker image id: build-and-push - uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0 + uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0 with: context: . push: true diff --git a/.github/workflows/build-and-push-ogcard-aws.yaml b/.github/workflows/build-and-push-ogcard-aws.yaml index a2d0c738cc..e1260d8f24 100644 --- a/.github/workflows/build-and-push-ogcard-aws.yaml +++ b/.github/workflows/build-and-push-ogcard-aws.yaml @@ -25,10 +25,10 @@ jobs: uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - name: Setup Docker buildx - uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0 + uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0 - name: Log into registry ${{ env.REGISTRY }} - uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0 + uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4.4.0 with: registry: ${{ env.REGISTRY }} username: ${{ env.USERNAME}} @@ -36,7 +36,7 @@ jobs: - name: Extract Docker metadata id: meta - uses: docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9 # v6.1.0 + uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6.2.0 with: images: | ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} @@ -45,7 +45,7 @@ jobs: - name: Build and push Docker image id: build-and-push - uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0 + uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0 with: context: . push: true diff --git a/.github/workflows/claude-mention.yml b/.github/workflows/claude-mention.yml index 0a87efb2aa..4c4b6236f0 100644 --- a/.github/workflows/claude-mention.yml +++ b/.github/workflows/claude-mention.yml @@ -65,7 +65,7 @@ jobs: aws-region: us-east-2 - name: Claude - uses: anthropics/claude-code-action@4633baf5267540f3f8cb58b684f79901d564c280 # v1.0.160 + uses: anthropics/claude-code-action@f87768c6d25f92ae6efa7175e223ef77d4cbf97f # v1.0.166 with: use_bedrock: 'true' additional_permissions: | diff --git a/.github/workflows/claude-review.yml b/.github/workflows/claude-review.yml index 045006f82b..a66147a6e3 100644 --- a/.github/workflows/claude-review.yml +++ b/.github/workflows/claude-review.yml @@ -51,7 +51,7 @@ jobs: aws-region: us-east-2 - name: Claude review - uses: anthropics/claude-code-action@4633baf5267540f3f8cb58b684f79901d564c280 # v1.0.160 + uses: anthropics/claude-code-action@f87768c6d25f92ae6efa7175e223ef77d4cbf97f # v1.0.166 with: use_bedrock: 'true' additional_permissions: | diff --git a/.github/workflows/pull-request-comment.yml b/.github/workflows/pull-request-comment.yml index 0d2b74aeec..46b169e541 100644 --- a/.github/workflows/pull-request-comment.yml +++ b/.github/workflows/pull-request-comment.yml @@ -106,7 +106,7 @@ jobs: core.setOutput('head-ref', pr.data.head.ref); - name: 💬 Drop a comment - uses: marocchino/sticky-pull-request-comment@0ea0beb66eb9baf113663a64ec522f60e49231c0 # v3.0.4 + uses: marocchino/sticky-pull-request-comment@5770ad5eb8f42dd2c4f34da00c94c5381e49af88 # v3.0.5 with: header: pull-request-eas-build-${{ steps.pr-info.outputs.head-sha }} number: ${{ github.event.issue.number }} @@ -193,7 +193,7 @@ jobs: RUNTIME_VERSION: - name: 💬 Drop a comment - uses: marocchino/sticky-pull-request-comment@0ea0beb66eb9baf113663a64ec522f60e49231c0 # v3.0.4 + uses: marocchino/sticky-pull-request-comment@5770ad5eb8f42dd2c4f34da00c94c5381e49af88 # v3.0.5 env: ISSUE_NUMBER: ${{ github.event.issue.number }} with: @@ -210,7 +210,7 @@ jobs: *Generated by [PR labeler](https://github.com/expo/expo/actions/workflows/pr-labeler.yml) 🤖* - name: 💬 Drop a comment - uses: marocchino/sticky-pull-request-comment@0ea0beb66eb9baf113663a64ec522f60e49231c0 # v3.0.4 + uses: marocchino/sticky-pull-request-comment@5770ad5eb8f42dd2c4f34da00c94c5381e49af88 # v3.0.5 if: failure() with: header: pull-request-eas-build-${{ steps.pr-info.outputs.head-sha }} diff --git a/.github/workflows/pull-request-commit.yml b/.github/workflows/pull-request-commit.yml index 3d4dfb9bf0..aa20b8d417 100644 --- a/.github/workflows/pull-request-commit.yml +++ b/.github/workflows/pull-request-commit.yml @@ -99,7 +99,7 @@ jobs: excluded_assets: "(.+).chunk.js|(.+).js.map|(.+).json|(.+).png|(.+).svg|(.+).webp|(.+).jpg|(.+).ico" - name: 💬 Drop a comment - uses: marocchino/sticky-pull-request-comment@0ea0beb66eb9baf113663a64ec522f60e49231c0 # v3.0.4 + uses: marocchino/sticky-pull-request-comment@5770ad5eb8f42dd2c4f34da00c94c5381e49af88 # v3.0.5 with: header: bundle-diff message: | @@ -140,7 +140,7 @@ jobs: profile: pull-request - name: 💬 Drop a comment - uses: marocchino/sticky-pull-request-comment@0ea0beb66eb9baf113663a64ec522f60e49231c0 # v3.0.4 + uses: marocchino/sticky-pull-request-comment@5770ad5eb8f42dd2c4f34da00c94c5381e49af88 # v3.0.5 if: ${{ steps.fingerprint.outputs.includes-changes }} with: header: fingerprint-diff @@ -158,7 +158,7 @@ jobs: *Generated by [PR labeler](https://github.com/expo/expo/actions/workflows/pr-labeler.yml) 🤖* - name: 💬 Delete comment - uses: marocchino/sticky-pull-request-comment@0ea0beb66eb9baf113663a64ec522f60e49231c0 # v3.0.4 + uses: marocchino/sticky-pull-request-comment@5770ad5eb8f42dd2c4f34da00c94c5381e49af88 # v3.0.5 if: ${{ !steps.fingerprint.outputs.includes-changes }} with: header: fingerprint-diff From 6f69ded4929a945b4dead2bbd52c464a00fef4b5 Mon Sep 17 00:00:00 2001 From: pfrazee <1270099+pfrazee@users.noreply.github.com> Date: Tue, 14 Jul 2026 03:10:07 +0000 Subject: [PATCH 20/52] Nightly source-language update --- src/locale/locales/en/messages.po | 108 +++++++++++++++++++++++------- 1 file changed, 82 insertions(+), 26 deletions(-) diff --git a/src/locale/locales/en/messages.po b/src/locale/locales/en/messages.po index 2bc4a249ce..9f59df11c3 100644 --- a/src/locale/locales/en/messages.po +++ b/src/locale/locales/en/messages.po @@ -246,6 +246,29 @@ msgstr "{0} added to chat" msgid "{0} and {1} added to chat" msgstr "{0} and {1} added to chat" +#. Social proof on the likes stat; the bolded names are people the viewer follows who liked the post and are its only likes +#. placeholder {0}: nameLink(names[0]) +#. placeholder {0}: names[0].displayName +#. placeholder {1}: nameLink(names[1]) +#. placeholder {1}: names[1].displayName +#: src/screens/PostThread/components/LikesStat.tsx:127 +#: src/screens/PostThread/components/LikesStat.tsx:183 +msgid "{0} and {1} like this" +msgstr "{0} and {1} like this" + +#. Social proof on the likes stat; the bolded name is a person the viewer follows who liked the post, and the count is the remaining number of likes +#. placeholder {0}: nameLink(names[0]) +#. placeholder {1}: formatPostStatCount(others) +#. placeholder {2}: formatPostStatCount(others) +#: src/screens/PostThread/components/LikesStat.tsx:188 +msgid "{0} and {others, plural, one {{1} other} other {{2} others}} like this" +msgstr "{0} and {others, plural, one {{1} other} other {{2} others}} like this" + +#. placeholder {0}: names[0].displayName +#: src/screens/PostThread/components/LikesStat.tsx:129 +msgid "{0} and {othersLabel} like this" +msgstr "{0} and {othersLabel} like this" + #. placeholder {0}: profile.followsCount || 0 #: src/screens/Profile/Header/Metrics.tsx:49 msgid "{0} following" @@ -296,6 +319,14 @@ msgstr "{0} left" msgid "{0} left the group" msgstr "{0} left the group" +#. Social proof on the likes stat; the bolded name is a person the viewer follows who liked the post and is its only like +#. placeholder {0}: nameLink(names[0]) +#. placeholder {0}: names[0].displayName +#: src/screens/PostThread/components/LikesStat.tsx:130 +#: src/screens/PostThread/components/LikesStat.tsx:198 +msgid "{0} likes this" +msgstr "{0} likes this" + #. placeholder {0}: formatTime(currentTime) #. placeholder {1}: formatTime(duration) #: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/Scrubber.tsx:203 @@ -352,6 +383,21 @@ msgstr "{0}, " msgid "{0}, {1} and {memberCount, plural, one {# other} other {# others}} added to chat" msgstr "{0}, {1} and {memberCount, plural, one {# other} other {# others}} added to chat" +#. Social proof on the likes stat; the bolded names are people the viewer follows who liked the post, and the count is the remaining number of likes +#. placeholder {0}: nameLink(names[0]) +#. placeholder {1}: nameLink(names[1]) +#. placeholder {2}: formatPostStatCount(others) +#. placeholder {3}: formatPostStatCount(others) +#: src/screens/PostThread/components/LikesStat.tsx:173 +msgid "{0}, {1}, and {others, plural, one {{2} other} other {{3} others}} like this" +msgstr "{0}, {1}, and {others, plural, one {{2} other} other {{3} others}} like this" + +#. placeholder {0}: names[0].displayName +#. placeholder {1}: names[1].displayName +#: src/screens/PostThread/components/LikesStat.tsx:126 +msgid "{0}, {1}, and {othersLabel} like this" +msgstr "{0}, {1}, and {othersLabel} like this" + #. placeholder {0}: feed.displayName #. placeholder {1}: sanitizeHandle(feed.creatorHandle, '@') #. placeholder {2}: feed.likeCount || 0 @@ -725,6 +771,12 @@ msgstr "" msgid "{numMatches, plural, one {# contact found} other {# contacts found}}" msgstr "" +#. placeholder {0}: formatPostStatCount(others) +#. placeholder {1}: formatPostStatCount(others) +#: src/screens/PostThread/components/LikesStat.tsx:119 +msgid "{others, plural, one {{0} other} other {{1} others}}" +msgstr "{others, plural, one {{0} other} other {{1} others}}" + #: src/components/NewskieDialog.tsx:115 msgid "{profileName} joined Bluesky {timeAgoString} ago" msgstr "" @@ -819,34 +871,33 @@ msgstr "" msgid "<0>{0} {1, plural, one {following} other {following}}" msgstr "" -#. Like count display, the <0> tags enclose the number of likes in bold (will never be 0) -#. placeholder {0}: formatPostStatCount(post.likeCount) -#. placeholder {1}: post.likeCount -#: src/screens/PostThread/components/ThreadItemAnchor.tsx:491 -msgid "<0>{0} {1, plural, one {like} other {likes}}" -msgstr "" - #. Quote count display, the <0> tags enclose the number of quotes in bold (will never be 0) #. placeholder {0}: formatPostStatCount(post.quoteCount) #. placeholder {1}: post.quoteCount -#: src/screens/PostThread/components/ThreadItemAnchor.tsx:472 +#: src/screens/PostThread/components/ThreadItemAnchor.tsx:470 msgid "<0>{0} {1, plural, one {quote} other {quotes}}" msgstr "" #. Repost count display, the <0> tags enclose the number of reposts in bold (will never be 0) #. placeholder {0}: formatPostStatCount(post.repostCount) #. placeholder {1}: post.repostCount -#: src/screens/PostThread/components/ThreadItemAnchor.tsx:451 +#: src/screens/PostThread/components/ThreadItemAnchor.tsx:449 msgid "<0>{0} {1, plural, one {repost} other {reposts}}" msgstr "" #. Save count display, the <0> tags enclose the number of saves in bold (will never be 0) #. placeholder {0}: formatPostStatCount(post.bookmarkCount) #. placeholder {1}: post.bookmarkCount -#: src/screens/PostThread/components/ThreadItemAnchor.tsx:509 +#: src/screens/PostThread/components/ThreadItemAnchor.tsx:488 msgid "<0>{0} {1, plural, one {save} other {saves}}" msgstr "" +#. Like count display, the <0> tags enclose the number of likes in bold (will never be 0) +#. placeholder {0}: formatPostStatCount(likeCount) +#: src/screens/PostThread/components/LikesStat.tsx:87 +msgid "<0>{0} {likeCount, plural, one {like} other {likes}}" +msgstr "<0>{0} {likeCount, plural, one {like} other {likes}}" + #. placeholder {0}: getName(items[0]) #. placeholder {1}: getName(items[1] /* [0] is self, skip it */) #. placeholder {1}: getName(items[1]) @@ -1849,12 +1900,12 @@ msgid "Apply Pull Request" msgstr "" #. placeholder {0}: niceDate(i18n, createdAt, 'medium') -#: src/screens/PostThread/components/ThreadItemAnchor.tsx:631 +#: src/screens/PostThread/components/ThreadItemAnchor.tsx:610 msgid "Archived from {0}" msgstr "" -#: src/screens/PostThread/components/ThreadItemAnchor.tsx:602 -#: src/screens/PostThread/components/ThreadItemAnchor.tsx:640 +#: src/screens/PostThread/components/ThreadItemAnchor.tsx:581 +#: src/screens/PostThread/components/ThreadItemAnchor.tsx:619 msgid "Archived post" msgstr "" @@ -2231,7 +2282,7 @@ msgstr "bloomscrolling booksky" msgid "Bluesky" msgstr "" -#: src/screens/PostThread/components/ThreadItemAnchor.tsx:656 +#: src/screens/PostThread/components/ThreadItemAnchor.tsx:635 msgid "Bluesky cannot confirm the authenticity of the claimed date." msgstr "" @@ -5777,6 +5828,11 @@ msgstr "" msgid "Go live for" msgstr "" +#. placeholder {0}: name.displayName +#: src/screens/PostThread/components/LikesStat.tsx:144 +msgid "Go to {0}'s profile" +msgstr "Go to {0}'s profile" + #: src/view/com/notifications/NotificationFeedItem.tsx:250 msgid "Go to {firstAuthorName}'s profile" msgstr "" @@ -6055,7 +6111,7 @@ msgstr "" #: src/lib/moderation/useLabelBehaviorDescription.ts:23 #: src/lib/moderation/useLabelBehaviorDescription.ts:28 #: src/lib/moderation/useLabelBehaviorDescription.ts:33 -#: src/view/shell/desktop/SidebarTrendingTopics.tsx:131 +#: src/view/shell/desktop/SidebarTrendingTopics.tsx:128 msgid "Hide" msgstr "" @@ -6120,7 +6176,7 @@ msgid "Hide trending topics" msgstr "" #: src/components/interstitials/Trending.tsx:131 -#: src/view/shell/desktop/SidebarTrendingTopics.tsx:129 +#: src/view/shell/desktop/SidebarTrendingTopics.tsx:126 msgid "Hide trending topics?" msgstr "" @@ -7028,7 +7084,7 @@ msgstr "" msgid "Likes of your reposts" msgstr "" -#: src/screens/PostThread/components/ThreadItemAnchor.tsx:487 +#: src/screens/PostThread/components/LikesStat.tsx:82 msgid "Likes on this post" msgstr "" @@ -8297,7 +8353,7 @@ msgstr "" #: src/components/dms/InitiateChatFlow.tsx:733 #: src/components/dms/MessageItem.tsx:742 #: src/screens/Login/PasswordUpdatedForm.tsx:35 -#: src/screens/PostThread/components/ThreadItemAnchor.tsx:662 +#: src/screens/PostThread/components/ThreadItemAnchor.tsx:641 msgid "Okay" msgstr "" @@ -9209,7 +9265,7 @@ msgstr "" msgid "Post failed to upload. Please check your Internet connection and try again." msgstr "" -#: src/screens/PostThread/components/ThreadItemAnchor.tsx:131 +#: src/screens/PostThread/components/ThreadItemAnchor.tsx:132 #: src/screens/PostThread/components/ThreadItemPost.tsx:116 #: src/screens/PostThread/components/ThreadItemTreePost.tsx:109 #: src/screens/VideoFeed/index.tsx:547 @@ -9493,7 +9549,7 @@ msgstr "" msgid "Quotes" msgstr "" -#: src/screens/PostThread/components/ThreadItemAnchor.tsx:468 +#: src/screens/PostThread/components/ThreadItemAnchor.tsx:466 msgid "Quotes of this post" msgstr "" @@ -10101,7 +10157,7 @@ msgstr "" msgid "Reposts" msgstr "" -#: src/screens/PostThread/components/ThreadItemAnchor.tsx:447 +#: src/screens/PostThread/components/ThreadItemAnchor.tsx:445 msgid "Reposts of this post" msgstr "" @@ -11211,7 +11267,7 @@ msgstr "" msgid "Show when you’re live" msgstr "" -#: src/screens/PostThread/components/ThreadItemAnchor.tsx:603 +#: src/screens/PostThread/components/ThreadItemAnchor.tsx:582 msgid "Shows information about when this post was created" msgstr "" @@ -12498,7 +12554,7 @@ msgstr "This person is blocking you" #. placeholder {0}: niceDate(i18n, createdAt) #. placeholder {1}: niceDate(i18n, indexedAt) -#: src/screens/PostThread/components/ThreadItemAnchor.tsx:643 +#: src/screens/PostThread/components/ThreadItemAnchor.tsx:622 msgid "This post claims to have been created on <0>{0}, but was first seen by Bluesky on <1>{1}." msgstr "" @@ -12750,7 +12806,7 @@ msgstr "Translation to the same language is unavailable on your device." msgid "Tree view" msgstr "" -#: src/view/shell/desktop/SidebarTrendingTopics.tsx:50 +#: src/view/shell/desktop/SidebarTrendingTopics.tsx:48 msgid "Trending" msgstr "" @@ -12759,7 +12815,7 @@ msgstr "" msgid "Trending GIFs" msgstr "Trending GIFs" -#: src/view/shell/desktop/SidebarTrendingTopics.tsx:57 +#: src/view/shell/desktop/SidebarTrendingTopics.tsx:55 msgid "Trending options" msgstr "" @@ -14289,7 +14345,7 @@ msgstr "You can select up to {MAX_GALLERY_IMAGES, plural, other {# images}} in t #: src/components/interstitials/Trending.tsx:132 #: src/components/interstitials/TrendingVideos.tsx:138 -#: src/view/shell/desktop/SidebarTrendingTopics.tsx:130 +#: src/view/shell/desktop/SidebarTrendingTopics.tsx:127 msgid "You can update this later from your settings." msgstr "" From 4b5138fdc1b2b5779a93ce03ec31c0ea7803cb45 Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Tue, 14 Jul 2026 15:39:05 +0300 Subject: [PATCH 21/52] Update Sentry to 8.x (#11147) Co-authored-by: Claude Fable 5 --- metro.config.js | 40 --- metro.config.ts | 57 ++++ package.json | 2 +- ...entry__expo-upload-sourcemaps@8.18.0.patch | 14 + ...ry__expo-upload-sourcemaps@8.18.0.patch.md | 7 + patches/@sentry__react-native@6.20.0.patch | 33 -- patches/@sentry__react-native@6.20.0.patch.md | 9 - pnpm-lock.yaml | 310 +++++++++--------- pnpm-workspace.yaml | 2 +- src/logger/sentry/setup/index.ts | 2 +- 10 files changed, 239 insertions(+), 237 deletions(-) delete mode 100644 metro.config.js create mode 100644 metro.config.ts create mode 100644 patches/@sentry__expo-upload-sourcemaps@8.18.0.patch create mode 100644 patches/@sentry__expo-upload-sourcemaps@8.18.0.patch.md delete mode 100644 patches/@sentry__react-native@6.20.0.patch delete mode 100644 patches/@sentry__react-native@6.20.0.patch.md diff --git a/metro.config.js b/metro.config.js deleted file mode 100644 index 363956a77d..0000000000 --- a/metro.config.js +++ /dev/null @@ -1,40 +0,0 @@ -// Learn more https://docs.expo.io/guides/customizing-metro -const {getSentryExpoConfig} = require('@sentry/react-native/metro') -const cfg = getSentryExpoConfig(__dirname) - -// inject `.e2e.ts` and `.e2e.tsx` into the sourceExts when running tests -cfg.resolver.sourceExts = process.env.RN_SRC_EXT - ? process.env.RN_SRC_EXT.split(',').concat(cfg.resolver.sourceExts) - : cfg.resolver.sourceExts - -if (cfg.resolver.resolveRequest) { - throw Error('Update this override because it is conflicting now.') -} - -if (process.env.BSKY_PROFILE) { - cfg.cacheVersion += ':PROFILE' -} - -cfg.resolver.assetExts = [...cfg.resolver.assetExts, 'woff2'] - -cfg.resolver.resolveRequest = (context, moduleName, platform) => { - if (process.env.BSKY_PROFILE) { - if (moduleName.endsWith('ReactNativeRenderer-prod')) { - return context.resolveRequest( - context, - moduleName.replace('-prod', '-profiling'), - platform, - ) - } - } - return context.resolveRequest(context, moduleName, platform) -} - -cfg.transformer.getTransformOptions = async () => ({ - transform: { - experimentalImportSupport: true, - inlineRequires: true, - }, -}) - -module.exports = cfg diff --git a/metro.config.ts b/metro.config.ts new file mode 100644 index 0000000000..070188c7c7 --- /dev/null +++ b/metro.config.ts @@ -0,0 +1,57 @@ +// Learn more https://docs.expo.io/guides/customizing-metro +import {type CustomResolver} from '@expo/metro/metro-resolver' +import {getDefaultConfig} from '@expo/metro-config' +import {getSentryExpoConfig} from '@sentry/react-native/metro.js' + +const config = getSentryExpoConfig(import.meta.dirname, { + // TODO: confirm this doesn't break anything when we switch to metro web + includeWebReplay: false, + annotateReactComponents: { + textComponentNames: ['Text', 'ButtonText'], + }, + getDefaultConfig: (projectRoot, options) => { + const config = getDefaultConfig(projectRoot, options) + + if (typeof process.env.RN_SRC_EXT === 'string') { + // inject `.e2e.ts` and `.e2e.tsx` into the sourceExts when running tests) + config.resolver.sourceExts.unshift(...process.env.RN_SRC_EXT.split(',')) + } + + config.resolver.assetExts = [...config.resolver.assetExts, 'woff2'] + + if (config.resolver.resolveRequest) { + throw Error('Update this override because it is conflicting now.') + } + + if (process.env.BSKY_PROFILE) { + // @ts-expect-error readonly property + config.cacheVersion += ':PROFILE' + + const resolver: CustomResolver = (context, moduleName, platform) => { + if (moduleName.endsWith('ReactNativeRenderer-prod')) { + return context.resolveRequest( + context, + moduleName.replace('-prod', '-profiling'), + platform, + ) + } + return context.resolveRequest(context, moduleName, platform) + } + + // @ts-expect-error readonly property + config.resolver.resolveRequest = resolver + } + + config.transformer.getTransformOptions = () => + Promise.resolve({ + transform: { + experimentalImportSupport: true, + inlineRequires: true as false, // ??? typescript why? + }, + }) + + return config as unknown as Record + }, +}) + +export default config diff --git a/package.json b/package.json index 0ffd9d742e..3dd3663a32 100644 --- a/package.json +++ b/package.json @@ -128,7 +128,7 @@ "@react-navigation/bottom-tabs": "^7.15.5", "@react-navigation/native": "^7.1.33", "@react-navigation/native-stack": "^7.14.4", - "@sentry/react-native": "~6.20.0", + "@sentry/react-native": "~8.18.0", "@tanstack/query-async-storage-persister": "^5.96.2", "@tanstack/react-query": "^5.96.2", "@tanstack/react-query-persist-client": "^5.96.2", diff --git a/patches/@sentry__expo-upload-sourcemaps@8.18.0.patch b/patches/@sentry__expo-upload-sourcemaps@8.18.0.patch new file mode 100644 index 0000000000..6246e99203 --- /dev/null +++ b/patches/@sentry__expo-upload-sourcemaps@8.18.0.patch @@ -0,0 +1,14 @@ +diff --git a/cli.js b/cli.js +index 2a2e6afbd46a6a5182918373d71853193945ff31..44e5888e869003438fc7ef4931b9992f2896a23f 100755 +--- a/cli.js ++++ b/cli.js +@@ -304,6 +304,9 @@ for (const [assetGroupName, assets] of Object.entries(groupedAssets)) { + if (isHermes) { + args.push('--debug-id-reference'); + } ++ if (process.env.SENTRY_DIST) { ++ args.push('--dist', process.env.SENTRY_DIST); ++ } + args.push(...assets); + + const result = spawnSync(sentryCliBin, args, { diff --git a/patches/@sentry__expo-upload-sourcemaps@8.18.0.patch.md b/patches/@sentry__expo-upload-sourcemaps@8.18.0.patch.md new file mode 100644 index 0000000000..6c09e13261 --- /dev/null +++ b/patches/@sentry__expo-upload-sourcemaps@8.18.0.patch.md @@ -0,0 +1,7 @@ +# @sentry/expo-upload-sourcemaps patch + +Adds `--dist $SENTRY_DIST` to the `sentry-cli sourcemaps upload` call, when the +env var is set. Symbolication matches on debug IDs so this isn't strictly +required, but it keeps OTA sourcemap artifacts associated with the commit hash +(`dist`) in the Sentry UI, matching the runtime `dist` set in +`src/logger/sentry/setup/index.ts`. diff --git a/patches/@sentry__react-native@6.20.0.patch b/patches/@sentry__react-native@6.20.0.patch deleted file mode 100644 index e8869040be..0000000000 --- a/patches/@sentry__react-native@6.20.0.patch +++ /dev/null @@ -1,33 +0,0 @@ -diff --git a/dist/js/tools/sentryMetroSerializer.js b/dist/js/tools/sentryMetroSerializer.js -index d7f2350196fc008f9f1ce530e224fe3d052347e9..4ce7b6614e38c8af747ebcf166dba556affea3de 100644 ---- a/dist/js/tools/sentryMetroSerializer.js -+++ b/dist/js/tools/sentryMetroSerializer.js -@@ -12,12 +12,9 @@ exports.createSentryMetroSerializer = exports.unstable_beforeAssetSerializationP - const crypto = require("crypto"); - const utils_1 = require("./utils"); - const utils_2 = require("./vendor/metro/utils"); --let countLines; --try { -- countLines = require('metro/private/lib/countLines'); --} --catch (e) { -- countLines = require('metro/src/lib/countLines'); -+const newline = /\r\n?|\n|\u2028|\u2029/g; -+function countLines(string) { -+ return (string.match(newline) || []).length + 1; - } - const DEBUG_ID_PLACE_HOLDER = '__debug_id_place_holder__'; - const DEBUG_ID_MODULE_PATH = '__debugid__'; -diff --git a/scripts/expo-upload-sourcemaps.js b/scripts/expo-upload-sourcemaps.js -index b3783b572171482778d31a96b8d6ebadbcc8783b..d5e3e45477c07b5419285237372d99ddd83c56a6 100755 ---- a/scripts/expo-upload-sourcemaps.js -+++ b/scripts/expo-upload-sourcemaps.js -@@ -218,7 +218,7 @@ for (const [assetGroupName, assets] of Object.entries(groupedAssets)) { - - const isHermes = assets.find(asset => asset.endsWith('.hbc')); - const windowsCallback = process.platform === "win32" ? 'node ' : ''; -- execSync(`${windowsCallback}${sentryCliBin} sourcemaps upload ${isHermes ? '--debug-id-reference' : ''} ${assets.join(' ')}`, { -+ execSync(`${windowsCallback}${sentryCliBin} sourcemaps upload ${isHermes ? '--debug-id-reference' : ''} ${assets.join(' ')} --dist ${process.env.SENTRY_DIST}`, { - env: { - ...process.env, - [SENTRY_PROJECT]: sentryProject, diff --git a/patches/@sentry__react-native@6.20.0.patch.md b/patches/@sentry__react-native@6.20.0.patch.md deleted file mode 100644 index d358fb4449..0000000000 --- a/patches/@sentry__react-native@6.20.0.patch.md +++ /dev/null @@ -1,9 +0,0 @@ -# @sentry/react-native/scripts/expo-upload-sourcemaps.js patch - -Lets us specify the output directory for the sourcemaps via an environment variable. - -# @sentry/react-native/dist/js/tools/sentryMetroSerializer.js patch - -Patch of this: https://github.com/getsentry/sentry-react-native/issues/5180#issuecomment-3311772038 - -Will be fixed in an upcoming release of @sentry/react-native - remove when available. diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f6bd990946..70e5e2ccf1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -215,7 +215,7 @@ overrides: react-native-screens: 4.24.0 patchedDependencies: - '@sentry/react-native@6.20.0': 1d48e4d5178f5684eb33262299e7eed283bf9601f79b9ae1af63a7f607d3483a + '@sentry/expo-upload-sourcemaps@8.18.0': 2368e1e1986165e7ead3c60017b78b2f9ade1b6a6b2f5ad27cd428608c6f12ae expo-age-range@0.2.18: c325225749993424461eeece1d97a99b19c00da2ebc958a73c12e4eca0c39306 expo-glass-effect@55.0.8: 6c9fa5b104e53b87df4e17b320acb3b94d12ac90c0101190045dd620681efff0 expo-haptics@15.0.8: b33910ba2753c4eccdc885b449e6e33aa90b9cefa75ca8639762a26013141410 @@ -347,8 +347,8 @@ importers: specifier: ^7.14.4 version: 7.15.0(@react-navigation/native@7.2.4(react-native@0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native-safe-area-context@5.6.2(react-native@0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native-screens@4.24.0(react-native@0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) '@sentry/react-native': - specifier: ~6.20.0 - version: 6.20.0(patch_hash=1d48e4d5178f5684eb33262299e7eed283bf9601f79b9ae1af63a7f607d3483a)(expo@54.0.34(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) + specifier: ~8.18.0 + version: 8.18.0(@expo/env@2.0.11)(dotenv@16.6.1)(expo@54.0.34(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) '@tanstack/query-async-storage-persister': specifier: ^5.96.2 version: 5.100.10 @@ -3237,53 +3237,35 @@ packages: '@remirror/core-constants@3.0.0': resolution: {integrity: sha512-42aWfPrimMfDKDi4YegyS7x+/0tlzaqwPQCULLanv3DMIlu96KTJR0fM5isWX2UViOqlGnX6YFgqWepcX+XMNg==} - '@sentry-internal/browser-utils@8.55.0': - resolution: {integrity: sha512-ROgqtQfpH/82AQIpESPqPQe0UyWywKJsmVIqi3c5Fh+zkds5LUxnssTj3yNd1x+kxaPDVB023jAP+3ibNgeNDw==} - engines: {node: '>=14.18'} - - '@sentry-internal/feedback@8.55.0': - resolution: {integrity: sha512-cP3BD/Q6pquVQ+YL+rwCnorKuTXiS9KXW8HNKu4nmmBAyf7urjs+F6Hr1k9MXP5yQ8W3yK7jRWd09Yu6DHWOiw==} - engines: {node: '>=14.18'} - - '@sentry-internal/replay-canvas@8.55.0': - resolution: {integrity: sha512-nIkfgRWk1091zHdu4NbocQsxZF1rv1f7bbp3tTIlZYbrH62XVZosx5iHAuZG0Zc48AETLE7K4AX9VGjvQj8i9w==} - engines: {node: '>=14.18'} - - '@sentry-internal/replay@8.55.0': - resolution: {integrity: sha512-roCDEGkORwolxBn8xAKedybY+Jlefq3xYmgN2fr3BTnsXjSYOPC7D1/mYqINBat99nDtvgFvNfRcZPiwwZ1hSw==} - engines: {node: '>=14.18'} - '@sentry/babel-plugin-component-annotate@3.6.1': resolution: {integrity: sha512-zmvUa4RpzDG3LQJFpGCE8lniz8Rk1Wa6ZvvK+yEH+snZeaHHRbSnAQBMR607GOClP+euGHNO2YtaY4UAdNTYbg==} engines: {node: '>= 14'} - '@sentry/babel-plugin-component-annotate@4.1.1': - resolution: {integrity: sha512-HUpqrCK7zDVojTV6KL6BO9ZZiYrEYQqvYQrscyMsq04z+WCupXaH6YEliiNRvreR8DBJgdsG3lBRpebhUGmvfA==} - engines: {node: '>= 14'} + '@sentry/babel-plugin-component-annotate@5.3.0': + resolution: {integrity: sha512-p4q8gn8wcFqZGP/s2MnJCAAd8fTikaU6A0mM97RDHQgStcrYiaS0Sc5zUNfb1V+UOLPuvdEdL6MwyxfzjYJQTA==} + engines: {node: '>= 18'} - '@sentry/browser@8.55.0': - resolution: {integrity: sha512-1A31mCEWCjaMxJt6qGUK+aDnLDcK6AwLAZnqpSchNysGni1pSn1RWSmk9TBF8qyTds5FH8B31H480uxMPUJ7Cw==} - engines: {node: '>=14.18'} + '@sentry/browser-utils@10.64.0': + resolution: {integrity: sha512-qXvUpsZdE0+6SovhDZvjN4JkqQEpzeYsrOF5g63wMsqJWWv2vW/pQZIlJs0F6C0t4q5AHZL4fl5rNkvpVqFdKA==} + engines: {node: '>=18'} + + '@sentry/browser@10.64.0': + resolution: {integrity: sha512-CHZ7+79evh8knBtVHjW/XqV3mRaWs2TdjX7i55zpFe9vcNngQHV++0ss8ird/xfMY1mfCDtbyAN+Z6XY2o5aJA==} + engines: {node: '>=18'} '@sentry/bundler-plugin-core@3.6.1': resolution: {integrity: sha512-/ubWjPwgLep84sUPzHfKL2Ns9mK9aQrEX4aBFztru7ygiJidKJTxYGtvjh4dL2M1aZ0WRQYp+7PF6+VKwdZXcQ==} engines: {node: '>= 14'} - '@sentry/cli-darwin@2.51.1': - resolution: {integrity: sha512-R1u8IQdn/7Rr8sf6bVVr0vJT4OqwCFdYsS44Y3OoWGVJW2aAQTWRJOTlV4ueclVLAyUQzmgBjfR8AtiUhd/M5w==} - engines: {node: '>=10'} - os: [darwin] - '@sentry/cli-darwin@2.58.5': resolution: {integrity: sha512-lYrNzenZFJftfwSya7gwrHGxtE+Kob/e1sr9lmHMFOd4utDlmq0XFDllmdZAMf21fxcPRI1GL28ejZ3bId01fQ==} engines: {node: '>=10'} os: [darwin] - '@sentry/cli-linux-arm64@2.51.1': - resolution: {integrity: sha512-nvA/hdhsw4bKLhslgbBqqvETjXwN1FVmwHLOrRvRcejDO6zeIKUElDiL5UOjGG0NC+62AxyNw5ri8Wzp/7rg9Q==} - engines: {node: '>=10'} - cpu: [arm64] - os: [linux, freebsd, android] + '@sentry/cli-darwin@3.6.0': + resolution: {integrity: sha512-C2SWHKaEP8NoYkLDr5jwrzklTwlJkzPIx7lu2LZrwBuHG/sNhWdili0ED2mD86b6Q90hlcMtuBm5IHUwcZ6FTQ==} + engines: {node: '>=18'} + os: [darwin] '@sentry/cli-linux-arm64@2.58.5': resolution: {integrity: sha512-/4gywFeBqRB6tR/iGMRAJ3HRqY6Z7Yp4l8ZCbl0TDLAfHNxu7schEw4tSnm2/Hh9eNMiOVy4z58uzAWlZXAYBQ==} @@ -3291,10 +3273,10 @@ packages: cpu: [arm64] os: [linux, freebsd, android] - '@sentry/cli-linux-arm@2.51.1': - resolution: {integrity: sha512-Klro17OmSSKOOSaxVKBBNPXet2+HrIDZUTSp8NRl4LQsIubdc1S/aQ79cH/g52Muwzpl3aFwPxyXw+46isfEgA==} - engines: {node: '>=10'} - cpu: [arm] + '@sentry/cli-linux-arm64@3.6.0': + resolution: {integrity: sha512-Rc+DB8vuTDpwqY2BxIPnooYk2ZDYQytF+n4nfi6pZZsJtr3SFFe+3wIWVmCVqxiHkaISb33+iJIDxOOqhkSNbQ==} + engines: {node: '>=18'} + cpu: [arm64] os: [linux, freebsd, android] '@sentry/cli-linux-arm@2.58.5': @@ -3303,10 +3285,10 @@ packages: cpu: [arm] os: [linux, freebsd, android] - '@sentry/cli-linux-i686@2.51.1': - resolution: {integrity: sha512-jp4TmR8VXBdT9dLo6mHniQHN0xKnmJoPGVz9h9VDvO2Vp/8o96rBc555D4Am5wJOXmfuPlyjGcmwHlB3+kQRWw==} - engines: {node: '>=10'} - cpu: [x86, ia32] + '@sentry/cli-linux-arm@3.6.0': + resolution: {integrity: sha512-S9xsDZTvybOGbrqqZ7DvF7JCNKp4cakDWJ4LdvQX+z82cHQSoLkYOXkA3EafDfWV9BGIRMIXitMMiSsV2PMU4g==} + engines: {node: '>=18'} + cpu: [arm] os: [linux, freebsd, android] '@sentry/cli-linux-i686@2.58.5': @@ -3315,10 +3297,10 @@ packages: cpu: [x86, ia32] os: [linux, freebsd, android] - '@sentry/cli-linux-x64@2.51.1': - resolution: {integrity: sha512-JuLt0MXM2KHNFmjqXjv23sly56mJmUQzGBWktkpY3r+jE08f5NLKPd5wQ6W/SoLXGIOKnwLz0WoUg7aBVyQdeQ==} - engines: {node: '>=10'} - cpu: [x64] + '@sentry/cli-linux-i686@3.6.0': + resolution: {integrity: sha512-PQ7+ctNmWtHgmbKa+rJheHU7D9GHJXafgWYfVW6gt7R0Ag9LxiDVYLGjrL4G/i7AGFNgudXFaLkGKNX7HUjc+g==} + engines: {node: '>=18'} + cpu: [x86, ia32] os: [linux, freebsd, android] '@sentry/cli-linux-x64@2.58.5': @@ -3327,11 +3309,11 @@ packages: cpu: [x64] os: [linux, freebsd, android] - '@sentry/cli-win32-arm64@2.51.1': - resolution: {integrity: sha512-PiwjTdIFDazTQCTyDCutiSkt4omggYSKnO3HE1+LDjElsFrWY9pJs4fU3D40WAyE2oKu0MarjNH/WxYGdqEAlg==} - engines: {node: '>=10'} - cpu: [arm64] - os: [win32] + '@sentry/cli-linux-x64@3.6.0': + resolution: {integrity: sha512-c+7xNg5BAaPE8N2Q6pg3Q/kt97JSaskuQIjRxHaFuDbCkJvww4VozY6mW5NUMJaW48rEs3mahWKamTLqJsO3wQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux, freebsd, android] '@sentry/cli-win32-arm64@2.58.5': resolution: {integrity: sha512-AOJ2nCXlQL1KBaCzv38m3i2VmSHNurUpm7xVKd6yAHX+ZoVBI8VT0EgvwmtJR2TY2N2hNCC7UrgRmdUsQ152bA==} @@ -3339,10 +3321,10 @@ packages: cpu: [arm64] os: [win32] - '@sentry/cli-win32-i686@2.51.1': - resolution: {integrity: sha512-TMvZZpeiI2HmrDFNVQ0uOiTuYKvjEGOZdmUxe3WlhZW82A/2Oka7sQ24ljcOovbmBOj5+fjCHRUMYvLMCWiysA==} - engines: {node: '>=10'} - cpu: [x86, ia32] + '@sentry/cli-win32-arm64@3.6.0': + resolution: {integrity: sha512-zhZ7YyGreHSKZ92Mwb9h4cEyL0I/eND7W6XIUXUW0BCCmxFOMc71vlQpUw8gijHIsFDbv8c8a6VOSkeRuwbSwQ==} + engines: {node: '>=18'} + cpu: [arm64] os: [win32] '@sentry/cli-win32-i686@2.58.5': @@ -3351,10 +3333,10 @@ packages: cpu: [x86, ia32] os: [win32] - '@sentry/cli-win32-x64@2.51.1': - resolution: {integrity: sha512-v2hreYUPPTNK1/N7+DeX7XBN/zb7p539k+2Osf0HFyVBaoUC3Y3+KBwSf4ASsnmgTAK7HCGR+X0NH1vP+icw4w==} - engines: {node: '>=10'} - cpu: [x64] + '@sentry/cli-win32-i686@3.6.0': + resolution: {integrity: sha512-JaxzVDdyetrPBp8NHh2yNAYdDk79ROXqfAfjQwG5z6V764MMMrf2WrhQ7EwoKXOPtBLm/drbOcYgaxHuDZKGRw==} + engines: {node: '>=18'} + cpu: [x86, ia32] os: [win32] '@sentry/cli-win32-x64@2.58.5': @@ -3363,22 +3345,49 @@ packages: cpu: [x64] os: [win32] - '@sentry/cli@2.51.1': - resolution: {integrity: sha512-FU+54kNcKJABU0+ekvtnoXHM9zVrDe1zXVFbQT7mS0On0m1P0zFRGdzbnWe2XzpzuEAJXtK6aog/W+esRU9AIA==} - engines: {node: '>= 10'} - hasBin: true + '@sentry/cli-win32-x64@3.6.0': + resolution: {integrity: sha512-LW0078VlxaUeVMMLA15A1zhkvZ5vby/lwthtBXPoBSDdTcgbTE4D4gQXM9vEapV28SvCO3fVIek3pbtrkaeDMw==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] '@sentry/cli@2.58.5': resolution: {integrity: sha512-tavJ7yGUZV+z3Ct2/ZB6mg339i08sAk6HDkgqmSRuQEu2iLS5sl9HIvuXfM6xjv8fwlgFOSy++WNABNAcGHUbg==} engines: {node: '>= 10'} hasBin: true - '@sentry/core@8.55.0': - resolution: {integrity: sha512-6g7jpbefjHYs821Z+EBJ8r4Z7LT5h80YSWRJaylGS4nW5W5Z2KXzpdnyFarv37O7QjauzVC2E+PABmpkw5/JGA==} - engines: {node: '>=14.18'} + '@sentry/cli@3.6.0': + resolution: {integrity: sha512-79XC8o59G/i3VqmnoQD74a/QD/422eQQlUqiPd3sEvcYCxnaZialRVAsxuNEFd6sx4aVGpqt775MMDT9cV/lMg==} + engines: {node: '>= 18'} + hasBin: true - '@sentry/react-native@6.20.0': - resolution: {integrity: sha512-YngSba14Hsb5t/ZNMOyxb/HInmYRL5pQ74BkoMBQ/UBBM5kWHgSILxoO2XkKYtaaJXrkSJj+kBalELHblz9h5g==} + '@sentry/conventions@0.15.1': + resolution: {integrity: sha512-ZLP8bRdMON3prWE2tJyImuYscCxdcJeIPIhrOs/rgyFm3C1nCh1B6gdvPj3AZ5zW08oSFFCsq7T+tYEW3h8MNA==} + engines: {node: '>=14'} + + '@sentry/core@10.64.0': + resolution: {integrity: sha512-HjojJcXD1l2qZ1AXje2s0XY/nYsaUt00wsM1HMBImA8vAClyPisFE/CC0/UD6pEvsGFhVgi8Dcxo7EN41uyeFw==} + engines: {node: '>=18'} + + '@sentry/expo-upload-sourcemaps@8.18.0': + resolution: {integrity: sha512-yJCJcG+gdGhYL11E6B59Upt0Txxb1RHTJ9df2CqfukNS723KRzg+kESEhUKHz9BQzYmvE8vnRvqobA3xA4j7Yw==} + engines: {node: '>=18'} + hasBin: true + peerDependencies: + '@expo/env': '*' + dotenv: '*' + peerDependenciesMeta: + '@expo/env': + optional: true + dotenv: + optional: true + + '@sentry/feedback@10.64.0': + resolution: {integrity: sha512-YrmKwRoAto+nwo26DoAhnpNmhkrVkuQFIAXqlTD8ipERjhcSNUYJAkAB+nH4w1KIgm9QQZE1sTq3iNQdPl1XMQ==} + engines: {node: '>=18'} + + '@sentry/react-native@8.18.0': + resolution: {integrity: sha512-W9jrTort+0LEkcezXRpIRMUaWXxbgvzEXlsuvCRU14n7yQiTfzKgFeSACpkzgqoW4iqJKphXoI9l4AVb54AdZg==} hasBin: true peerDependencies: expo: '>=49.0.0' @@ -3388,19 +3397,19 @@ packages: expo: optional: true - '@sentry/react@8.55.0': - resolution: {integrity: sha512-/qNBvFLpvSa/Rmia0jpKfJdy16d4YZaAnH/TuKLAtm0BWlsPQzbXCU4h8C5Hsst0Do0zG613MEtEmWpWrVOqWA==} - engines: {node: '>=14.18'} + '@sentry/react@10.64.0': + resolution: {integrity: sha512-oShEKcosBKdm0kgan8suFb6Jj8poccEyDz2qiflonq/5/hUDyzdVeKfFzf0b1MmdjWSn9k3hfrLNFfPNCuEt7Q==} + engines: {node: '>=18'} peerDependencies: react: ^16.14.0 || 17.x || 18.x || 19.x - '@sentry/types@8.55.0': - resolution: {integrity: sha512-6LRT0+r6NWQ+RtllrUW2yQfodST0cJnkOmdpHA75vONgBUhpKwiJ4H7AmgfoTET8w29pU6AnntaGOe0LJbOmog==} - engines: {node: '>=14.18'} + '@sentry/replay-canvas@10.64.0': + resolution: {integrity: sha512-EioBwcnnhtPiXzTZJTbZKaMEg3qNM5YZlX1VanoXomPATqfJD62cb/M27zhgiWXXJ7e8/NGVHvwNDYGrqsN39g==} + engines: {node: '>=18'} - '@sentry/utils@8.55.0': - resolution: {integrity: sha512-cYcl39+xcOivBpN9d8ZKbALl+DxZKo/8H0nueJZ0PO4JA+MJGhSm6oHakXxLPaiMoNLTX7yor8ndnQIuFg+vmQ==} - engines: {node: '>=14.18'} + '@sentry/replay@10.64.0': + resolution: {integrity: sha512-q8nke2GDSwEiu1zYoctDDvnSNTjHWoVAKo2JXTleD0nwOO6IlAgUYmsm3qEQiSW7BVla9W1TRbW6ChFAUXYZbw==} + engines: {node: '>=18'} '@sentry/webpack-plugin@3.6.1': resolution: {integrity: sha512-F2yqwbdxfCENMN5u4ih4WfOtGjW56/92DBC0bU6un7Ns/l2qd+wRONIvrF+58rl/VkCFfMlUtZTVoKGRyMRmHA==} @@ -12029,35 +12038,21 @@ snapshots: '@remirror/core-constants@3.0.0': {} - '@sentry-internal/browser-utils@8.55.0': - dependencies: - '@sentry/core': 8.55.0 - - '@sentry-internal/feedback@8.55.0': - dependencies: - '@sentry/core': 8.55.0 - - '@sentry-internal/replay-canvas@8.55.0': - dependencies: - '@sentry-internal/replay': 8.55.0 - '@sentry/core': 8.55.0 - - '@sentry-internal/replay@8.55.0': - dependencies: - '@sentry-internal/browser-utils': 8.55.0 - '@sentry/core': 8.55.0 - '@sentry/babel-plugin-component-annotate@3.6.1': {} - '@sentry/babel-plugin-component-annotate@4.1.1': {} + '@sentry/babel-plugin-component-annotate@5.3.0': {} - '@sentry/browser@8.55.0': + '@sentry/browser-utils@10.64.0': dependencies: - '@sentry-internal/browser-utils': 8.55.0 - '@sentry-internal/feedback': 8.55.0 - '@sentry-internal/replay': 8.55.0 - '@sentry-internal/replay-canvas': 8.55.0 - '@sentry/core': 8.55.0 + '@sentry/core': 10.64.0 + + '@sentry/browser@10.64.0': + dependencies: + '@sentry/browser-utils': 10.64.0 + '@sentry/core': 10.64.0 + '@sentry/feedback': 10.64.0 + '@sentry/replay': 10.64.0 + '@sentry/replay-canvas': 10.64.0 '@sentry/bundler-plugin-core@3.6.1': dependencies: @@ -12073,73 +12068,53 @@ snapshots: - encoding - supports-color - '@sentry/cli-darwin@2.51.1': - optional: true - '@sentry/cli-darwin@2.58.5': optional: true - '@sentry/cli-linux-arm64@2.51.1': + '@sentry/cli-darwin@3.6.0': optional: true '@sentry/cli-linux-arm64@2.58.5': optional: true - '@sentry/cli-linux-arm@2.51.1': + '@sentry/cli-linux-arm64@3.6.0': optional: true '@sentry/cli-linux-arm@2.58.5': optional: true - '@sentry/cli-linux-i686@2.51.1': + '@sentry/cli-linux-arm@3.6.0': optional: true '@sentry/cli-linux-i686@2.58.5': optional: true - '@sentry/cli-linux-x64@2.51.1': + '@sentry/cli-linux-i686@3.6.0': optional: true '@sentry/cli-linux-x64@2.58.5': optional: true - '@sentry/cli-win32-arm64@2.51.1': + '@sentry/cli-linux-x64@3.6.0': optional: true '@sentry/cli-win32-arm64@2.58.5': optional: true - '@sentry/cli-win32-i686@2.51.1': + '@sentry/cli-win32-arm64@3.6.0': optional: true '@sentry/cli-win32-i686@2.58.5': optional: true - '@sentry/cli-win32-x64@2.51.1': + '@sentry/cli-win32-i686@3.6.0': optional: true '@sentry/cli-win32-x64@2.58.5': optional: true - '@sentry/cli@2.51.1': - dependencies: - https-proxy-agent: 5.0.1 - node-fetch: 2.7.0 - progress: 2.0.3 - proxy-from-env: 1.1.0 - which: 2.0.2 - optionalDependencies: - '@sentry/cli-darwin': 2.51.1 - '@sentry/cli-linux-arm': 2.51.1 - '@sentry/cli-linux-arm64': 2.51.1 - '@sentry/cli-linux-i686': 2.51.1 - '@sentry/cli-linux-x64': 2.51.1 - '@sentry/cli-win32-arm64': 2.51.1 - '@sentry/cli-win32-i686': 2.51.1 - '@sentry/cli-win32-x64': 2.51.1 - transitivePeerDependencies: - - encoding - - supports-color + '@sentry/cli-win32-x64@3.6.0': + optional: true '@sentry/cli@2.58.5': dependencies: @@ -12161,39 +12136,70 @@ snapshots: - encoding - supports-color - '@sentry/core@8.55.0': {} - - '@sentry/react-native@6.20.0(patch_hash=1d48e4d5178f5684eb33262299e7eed283bf9601f79b9ae1af63a7f607d3483a)(expo@54.0.34(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)': + '@sentry/cli@3.6.0': dependencies: - '@sentry/babel-plugin-component-annotate': 4.1.1 - '@sentry/browser': 8.55.0 - '@sentry/cli': 2.51.1 - '@sentry/core': 8.55.0 - '@sentry/react': 8.55.0(react@19.1.0) - '@sentry/types': 8.55.0 - '@sentry/utils': 8.55.0 + progress: 2.0.3 + proxy-from-env: 1.1.0 + undici: 6.25.0 + which: 2.0.2 + optionalDependencies: + '@sentry/cli-darwin': 3.6.0 + '@sentry/cli-linux-arm': 3.6.0 + '@sentry/cli-linux-arm64': 3.6.0 + '@sentry/cli-linux-i686': 3.6.0 + '@sentry/cli-linux-x64': 3.6.0 + '@sentry/cli-win32-arm64': 3.6.0 + '@sentry/cli-win32-i686': 3.6.0 + '@sentry/cli-win32-x64': 3.6.0 + + '@sentry/conventions@0.15.1': {} + + '@sentry/core@10.64.0': + dependencies: + '@sentry/conventions': 0.15.1 + + '@sentry/expo-upload-sourcemaps@8.18.0(patch_hash=2368e1e1986165e7ead3c60017b78b2f9ade1b6a6b2f5ad27cd428608c6f12ae)(@expo/env@2.0.11)(dotenv@16.6.1)': + dependencies: + '@sentry/cli': 3.6.0 + optionalDependencies: + '@expo/env': 2.0.11 + dotenv: 16.6.1 + + '@sentry/feedback@10.64.0': + dependencies: + '@sentry/core': 10.64.0 + + '@sentry/react-native@8.18.0(@expo/env@2.0.11)(dotenv@16.6.1)(expo@54.0.34(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)': + dependencies: + '@sentry/babel-plugin-component-annotate': 5.3.0 + '@sentry/browser': 10.64.0 + '@sentry/cli': 3.6.0 + '@sentry/core': 10.64.0 + '@sentry/expo-upload-sourcemaps': 8.18.0(patch_hash=2368e1e1986165e7ead3c60017b78b2f9ade1b6a6b2f5ad27cd428608c6f12ae)(@expo/env@2.0.11)(dotenv@16.6.1) + '@sentry/react': 10.64.0(react@19.1.0) react: 19.1.0 react-native: 0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0) optionalDependencies: expo: 54.0.34(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) transitivePeerDependencies: - - encoding - - supports-color + - '@expo/env' + - dotenv - '@sentry/react@8.55.0(react@19.1.0)': + '@sentry/react@10.64.0(react@19.1.0)': dependencies: - '@sentry/browser': 8.55.0 - '@sentry/core': 8.55.0 - hoist-non-react-statics: 3.3.2 + '@sentry/browser': 10.64.0 + '@sentry/core': 10.64.0 react: 19.1.0 - '@sentry/types@8.55.0': + '@sentry/replay-canvas@10.64.0': dependencies: - '@sentry/core': 8.55.0 + '@sentry/core': 10.64.0 + '@sentry/replay': 10.64.0 - '@sentry/utils@8.55.0': + '@sentry/replay@10.64.0': dependencies: - '@sentry/core': 8.55.0 + '@sentry/browser-utils': 10.64.0 + '@sentry/core': 10.64.0 '@sentry/webpack-plugin@3.6.1(webpack@5.106.2(postcss@8.5.14))': dependencies: diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 688f5f058b..c917be629a 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -19,7 +19,7 @@ allowBuilds: 'esbuild': true 'unrs-resolver': true patchedDependencies: - '@sentry/react-native@6.20.0': patches/@sentry__react-native@6.20.0.patch + '@sentry/expo-upload-sourcemaps@8.18.0': patches/@sentry__expo-upload-sourcemaps@8.18.0.patch expo-age-range@0.2.18: patches/expo-age-range@0.2.18.patch 'expo-glass-effect@55.0.8': patches/expo-glass-effect@55.0.8.patch 'expo-haptics@15.0.8': patches/expo-haptics@15.0.8.patch diff --git a/src/logger/sentry/setup/index.ts b/src/logger/sentry/setup/index.ts index 4fe07e97e2..666fc3b0a6 100644 --- a/src/logger/sentry/setup/index.ts +++ b/src/logger/sentry/setup/index.ts @@ -4,7 +4,7 @@ import * as env from '#/env' init({ enabled: !env.IS_DEV && !!env.SENTRY_DSN, - autoSessionTracking: false, + enableAutoSessionTracking: false, dsn: env.SENTRY_DSN, debug: false, // If `true`, Sentry will try to print out useful debugging information if something goes wrong with sending the event. Set it to `false` in production environment: env.ENV, From ac0a249721c7f24b01e8322077d290b269c81eb4 Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Tue, 14 Jul 2026 15:52:56 +0300 Subject: [PATCH 22/52] Typescript 7 (#11109) --- package.json | 5 +- pnpm-lock.yaml | 254 ++++++++++++++++++++++++++++++++------------ pnpm-workspace.yaml | 22 ++++ 3 files changed, 211 insertions(+), 70 deletions(-) diff --git a/package.json b/package.json index 3dd3663a32..5fbbbf2f54 100644 --- a/package.json +++ b/package.json @@ -62,7 +62,7 @@ "lint": "oxlint --quiet src modules", "lint-native": "swiftlint ./modules && ktlint ./modules", "lint-native:fix": "swiftlint --fix ./modules && ktlint --format ./modules", - "typecheck": "tsgo --project ./tsconfig.check.json", + "typecheck": "tsc --project ./tsconfig.check.json", "e2e:mock-server": "cd dev-env && pnpm start", "e2e:build": "EXPO_PUBLIC_ENV=e2e NODE_ENV=test RN_SRC_EXT=e2e.ts,e2e.tsx expo run:ios", "e2e:build-android": "EXPO_PUBLIC_ENV=e2e NODE_ENV=test RN_SRC_EXT=e2e.ts,e2e.tsx expo run:android", @@ -270,7 +270,6 @@ "@types/psl": "1.1.1", "@types/react": "^19.1.17", "@types/react-dom": "^19.1.11", - "@typescript/native-preview": "^7.0.0-dev.20260428.1", "babel-jest": "^29.7.0", "babel-plugin-module-resolver": "^5.0.2", "babel-plugin-react-compiler": "19.1.0-rc.3", @@ -295,7 +294,7 @@ "react-refresh": "^0.14.0", "svgo": "^3.3.2", "ts-plugin-sort-import-suggestions": "^1.0.4", - "typescript": "^6.0.2", + "typescript": "^7.0.2", "webpack-bundle-analyzer": "^4.10.1" }, "jest": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 70e5e2ccf1..2d9099c437 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -330,10 +330,10 @@ importers: version: 9.2.7 '@lingui/core': specifier: ^5.9.2 - version: 5.9.5(@lingui/babel-plugin-lingui-macro@5.9.5(typescript@6.0.3)) + version: 5.9.5(@lingui/babel-plugin-lingui-macro@5.9.5(typescript@7.0.2)) '@lingui/react': specifier: ^5.9.2 - version: 5.9.5(@lingui/babel-plugin-lingui-macro@5.9.5(typescript@6.0.3))(react@19.1.0) + version: 5.9.5(@lingui/babel-plugin-lingui-macro@5.9.5(typescript@7.0.2))(react@19.1.0) '@react-native-async-storage/async-storage': specifier: 2.2.0 version: 2.2.0(react-native@0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0)) @@ -727,10 +727,10 @@ importers: version: 4.14.2 '@lingui/babel-plugin-lingui-macro': specifier: ^5.9.2 - version: 5.9.5(typescript@6.0.3) + version: 5.9.5(typescript@7.0.2) '@lingui/cli': specifier: ^5.9.2 - version: 5.9.5(typescript@6.0.3) + version: 5.9.5(typescript@7.0.2) '@pmmmwh/react-refresh-webpack-plugin': specifier: ^0.5.15 version: 0.5.17(react-refresh@0.14.2)(type-fest@1.4.0)(webpack-dev-server@4.15.2(webpack@5.106.2(postcss@8.5.14)))(webpack@5.106.2(postcss@8.5.14)) @@ -767,9 +767,6 @@ importers: '@types/react-dom': specifier: ^19.1.11 version: 19.1.11(@types/react@19.1.17) - '@typescript/native-preview': - specifier: ^7.0.0-dev.20260428.1 - version: 7.0.0-dev.20260512.1 babel-jest: specifier: ^29.7.0 version: 29.7.0(@babel/core@7.29.0) @@ -846,8 +843,8 @@ importers: specifier: ^1.0.4 version: 1.0.4 typescript: - specifier: ^6.0.2 - version: 6.0.3 + specifier: ^7.0.2 + version: 7.0.2 webpack-bundle-analyzer: specifier: ^4.10.1 version: 4.10.2 @@ -3715,53 +3712,126 @@ packages: '@types/yargs@17.0.35': resolution: {integrity: sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg==} - '@typescript/native-preview-darwin-arm64@7.0.0-dev.20260512.1': - resolution: {integrity: sha512-l9AJi/TIVMPx5R1c7fxZCSA7eUaHeA0C9Mxdxx/oQJo1K/GtbI3mzYe/SiKNltko1KSdKUmWVhPwxTOS289REg==} + '@typescript/typescript-aix-ppc64@7.0.2': + resolution: {integrity: sha512-MTKKkWB7p/0E9xi1d1tHtZ5PiLkGEMIq88pK2CubZjOsLtYTLqhgIgi6zepFa+9GHZ6h05NMCkQxGKiPXMxXtQ==} + engines: {node: '>=16.20.0'} + cpu: [ppc64] + os: [aix] + + '@typescript/typescript-darwin-arm64@7.0.2': + resolution: {integrity: sha512-gowzar9MwS/aRWp6f3a4KUqzRjAZjOsmGNCM6LcTgXum+dBfgsBVMN+AgvOCCbguXyick6LJhpBszxMebJ8syA==} engines: {node: '>=16.20.0'} cpu: [arm64] os: [darwin] - '@typescript/native-preview-darwin-x64@7.0.0-dev.20260512.1': - resolution: {integrity: sha512-oABZLQrfB8JN2Ct2CiLK5PyE28Em3sIJlZsAMD45/A2ymtIaa5826dwv8vapE5Wjp54ao0LXxCSuKFm1A8zzCQ==} + '@typescript/typescript-darwin-x64@7.0.2': + resolution: {integrity: sha512-SZ9xZInqApNlNGc9s0W1VSsktYSOe9cFqNOIqmN1Gs8SmkjKZYFt017G4VwPxASInODuAdbTW7sXiFUf893RgA==} engines: {node: '>=16.20.0'} cpu: [x64] os: [darwin] - '@typescript/native-preview-linux-arm64@7.0.0-dev.20260512.1': - resolution: {integrity: sha512-xvbwzpTe+5N6bnBI/t9n4zsGzXxz3V6rVbvDUoJmRLfav5fz+ck0QDkGQGUPrQEEIp0KEzQvx7c+AEZnzdvTQA==} + '@typescript/typescript-freebsd-arm64@7.0.2': + resolution: {integrity: sha512-W5NH4y/J0plIIS5b2xvTEkU7JFxyqdMAOgf+Ilhl0vHQXKO5dZoxd+C/jEtq56c4F3wk71RB4BMRQ2XdI+bwYQ==} + engines: {node: '>=16.20.0'} + cpu: [arm64] + os: [freebsd] + + '@typescript/typescript-freebsd-x64@7.0.2': + resolution: {integrity: sha512-UMGDx5sTpzNw3WiPebH7l90IWfJggEd+egHt/q6p7/Cm3zqoV7VxkGXt+3DxPIw8CcmvAB0j3sVVfbhX+M4Tpw==} + engines: {node: '>=16.20.0'} + cpu: [x64] + os: [freebsd] + + '@typescript/typescript-linux-arm64@7.0.2': + resolution: {integrity: sha512-Qh4eU4/y3yDjnfjjyPYihMj5/ODIlmt+Bzu17OI+fiSRDW57QmU5SiN63exPRNJPKUzcc1INa1NXdrJ+MqHjUQ==} engines: {node: '>=16.20.0'} cpu: [arm64] os: [linux] - '@typescript/native-preview-linux-arm@7.0.0-dev.20260512.1': - resolution: {integrity: sha512-0Hs1Gqa/t9cthoPdqHud1pFGUr9DgJivBTjwquTUh8jt/6PI2bQxoMNZLiN/bhqeDFDTzdxoMBfCaytsTMcXqw==} + '@typescript/typescript-linux-arm@7.0.2': + resolution: {integrity: sha512-gffT3xPz9sR7j/YJExkyPntrI0P2EP9XbOyWzth2/Gs0RstK+90RBcO0ncXoXy/beYll1SXw846Nf2zdnEz0QQ==} engines: {node: '>=16.20.0'} cpu: [arm] os: [linux] - '@typescript/native-preview-linux-x64@7.0.0-dev.20260512.1': - resolution: {integrity: sha512-qr5h6FPo74bN/U+EwRuayBhUbxaji8xzFbIbhMOA2oYSc/qozp5ia2g1+9xGw67MXxPPw/IPT+UGvrNK7K1NeQ==} + '@typescript/typescript-linux-loong64@7.0.2': + resolution: {integrity: sha512-uEHck9i8hoAzXPiYRib1O7miOnz23SxIeVl6F4LXox+qov1K35jHcEW6VHKvZI+pyvl7fZEP4MCU5LYvIq1GuQ==} + engines: {node: '>=16.20.0'} + cpu: [loong64] + os: [linux] + + '@typescript/typescript-linux-mips64el@7.0.2': + resolution: {integrity: sha512-R4KvAMnE43W5Qeqb0Ly56O3mWMWIAgsMyz36DCaycd5nbg/9kzm0liw3JocfRqyJY0KPmzFjbswozXyW0DnIYA==} + engines: {node: '>=16.20.0'} + cpu: [mips64el] + os: [linux] + + '@typescript/typescript-linux-ppc64@7.0.2': + resolution: {integrity: sha512-DORx5b3sd/4S7eayxm4FQv+A7CrkUIGRaHiwI8oiHTAI1fAPWhF4J0vAlkC8biAlHSVVwxMQ3tjZ2/DVbnQiiA==} + engines: {node: '>=16.20.0'} + cpu: [ppc64] + os: [linux] + + '@typescript/typescript-linux-riscv64@7.0.2': + resolution: {integrity: sha512-wf0jqEDOjrPRnKwYRyyJDRo11KMbvMFrU+q4zqKyChODBzvlkbhNQfKvLxQCcwTpdDaXSHZTVuh0JoCrKCUMHQ==} + engines: {node: '>=16.20.0'} + cpu: [riscv64] + os: [linux] + + '@typescript/typescript-linux-s390x@7.0.2': + resolution: {integrity: sha512-IkwJc3L7yhytWd/ewjyxNDfOmswCm9GWMJT/ue/dU4aZNbwZeYAetq42VyLmsmSjvoX7z74X6ZaYCtzAr0EuGw==} + engines: {node: '>=16.20.0'} + cpu: [s390x] + os: [linux] + + '@typescript/typescript-linux-x64@7.0.2': + resolution: {integrity: sha512-EYdf2cNg7rgCWJnxCdJ+F3V39O8ihb37eHAu1LK8oAFizgTQbPOK7zHHXbPt8rX24COqODXeI3sIf0fCXG7H/A==} engines: {node: '>=16.20.0'} cpu: [x64] os: [linux] - '@typescript/native-preview-win32-arm64@7.0.0-dev.20260512.1': - resolution: {integrity: sha512-meNWxhNEfaqos2U0JXvfxWvy4JWrKE9fZepCndDZi+t04X+AIiLYp5s6crWnKP67nzVAzMNgTAc8mu8CnGM+/A==} + '@typescript/typescript-netbsd-arm64@7.0.2': + resolution: {integrity: sha512-+polYF4MF04aPpO5FTkHran9yUQDSXqy5GiSDKpsll5jy3l3+g9QLhpf39T+ePtefhXLOGrLl0QIjkQP6VnelA==} + engines: {node: '>=16.20.0'} + cpu: [arm64] + os: [netbsd] + + '@typescript/typescript-netbsd-x64@7.0.2': + resolution: {integrity: sha512-8YIT0EHM/3dq10ZOVF/A7pc/YSMtbcecct4rWtexrnSCHOPcpC2KTLXfTCR6vDpnSiY12heNb1GiN/wu+T/FyA==} + engines: {node: '>=16.20.0'} + cpu: [x64] + os: [netbsd] + + '@typescript/typescript-openbsd-arm64@7.0.2': + resolution: {integrity: sha512-APT8+ClYnuYm1u9+kgGXoMj2VzWzcymwh2gNSQVySHfkRDGOTVkoWLjCmOQSaO+PoqQ57B0flRp9SA+7GnnkzQ==} + engines: {node: '>=16.20.0'} + cpu: [arm64] + os: [openbsd] + + '@typescript/typescript-openbsd-x64@7.0.2': + resolution: {integrity: sha512-yX7s+Q0Dln0Dt9tEzZsAjXXR/+ytBM7AlglaqyeMPxQszJ1JhlJdZ6jLA+IzldHtflX81em7lDao1xXu+aRRkg==} + engines: {node: '>=16.20.0'} + cpu: [x64] + os: [openbsd] + + '@typescript/typescript-sunos-x64@7.0.2': + resolution: {integrity: sha512-dLJDGaLZ1D4HPQn62u1n8mBDkJREwMsAkCdkwd4Ieqw+x3TUyTsqY0YiBCtE6H6OzzgGk3iuZ3vFWRS+E8/d1g==} + engines: {node: '>=16.20.0'} + cpu: [x64] + os: [sunos] + + '@typescript/typescript-win32-arm64@7.0.2': + resolution: {integrity: sha512-Gyl1Vy6OsWesLzmq+EP0Fb7b4Nid5232AvcA2SFcdYreldpNtYFFofPjnt62y9hQy7VTaZp65ICJjuAQRaVcIQ==} engines: {node: '>=16.20.0'} cpu: [arm64] os: [win32] - '@typescript/native-preview-win32-x64@7.0.0-dev.20260512.1': - resolution: {integrity: sha512-Hp6vBnxJSKEEAVWgIoWMmfqkZXCdkhm6XTivrwgRzBwWfiTVe2ZyZ7byWegIKeNnBbffg/K2KvoM8JAHl059GQ==} + '@typescript/typescript-win32-x64@7.0.2': + resolution: {integrity: sha512-0BQ3HkAHHlKLSp1qRvf3SUhGpGsDuhB/jgFw75guyqbxJqEaS0Cw/VFO8i2nHglJUzQCRtMMR/IBAKE3ETMC4g==} engines: {node: '>=16.20.0'} cpu: [x64] os: [win32] - '@typescript/native-preview@7.0.0-dev.20260512.1': - resolution: {integrity: sha512-KIzYPGuxZnyiiYkYrozDT94Af2nwbdLXoY1cgGY66RRa9HSEw13RH9WHg8wA8fZhT4wYzF5uF7WY3hz0QhaxGg==} - engines: {node: '>=16.20.0'} - hasBin: true - '@ungap/structured-clone@1.3.1': resolution: {integrity: sha512-mUFwbeTqrVgDQxFveS+df2yfap6iuP20NAKAsBt5jDEoOTDew+zwLAOilHCeQJOVSvmgCX4ogqIrA0mnyr08yQ==} @@ -8646,9 +8716,9 @@ packages: resolution: {integrity: sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==} engines: {node: '>= 0.4'} - typescript@6.0.3: - resolution: {integrity: sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==} - engines: {node: '>=14.17'} + typescript@7.0.2: + resolution: {integrity: sha512-8FYau96o3NKOhbjKi/qNvG/W5jhzxkbdm5sj9AbZ/5T5sWqn3hJgLfGx27sRKZWTvyzCP8dLRBTf5tBTSRVUNA==} + engines: {node: '>=16.20.0'} hasBin: true ua-parser-js@0.7.41: @@ -10902,19 +10972,19 @@ snapshots: '@lingui/babel-plugin-extract-messages@5.9.5': {} - '@lingui/babel-plugin-lingui-macro@5.9.5(typescript@6.0.3)': + '@lingui/babel-plugin-lingui-macro@5.9.5(typescript@7.0.2)': dependencies: '@babel/core': 7.29.0 '@babel/runtime': 7.29.2 '@babel/types': 7.29.0 - '@lingui/conf': 5.9.5(typescript@6.0.3) - '@lingui/core': 5.9.5(@lingui/babel-plugin-lingui-macro@5.9.5(typescript@6.0.3)) + '@lingui/conf': 5.9.5(typescript@7.0.2) + '@lingui/core': 5.9.5(@lingui/babel-plugin-lingui-macro@5.9.5(typescript@7.0.2)) '@lingui/message-utils': 5.9.5 transitivePeerDependencies: - supports-color - typescript - '@lingui/cli@5.9.5(typescript@6.0.3)': + '@lingui/cli@5.9.5(typescript@7.0.2)': dependencies: '@babel/core': 7.29.0 '@babel/generator': 7.29.1 @@ -10922,10 +10992,10 @@ snapshots: '@babel/runtime': 7.29.2 '@babel/types': 7.29.0 '@lingui/babel-plugin-extract-messages': 5.9.5 - '@lingui/babel-plugin-lingui-macro': 5.9.5(typescript@6.0.3) - '@lingui/conf': 5.9.5(typescript@6.0.3) - '@lingui/core': 5.9.5(@lingui/babel-plugin-lingui-macro@5.9.5(typescript@6.0.3)) - '@lingui/format-po': 5.9.5(typescript@6.0.3) + '@lingui/babel-plugin-lingui-macro': 5.9.5(typescript@7.0.2) + '@lingui/conf': 5.9.5(typescript@7.0.2) + '@lingui/core': 5.9.5(@lingui/babel-plugin-lingui-macro@5.9.5(typescript@7.0.2)) + '@lingui/format-po': 5.9.5(typescript@7.0.2) '@lingui/message-utils': 5.9.5 chokidar: 3.5.1 cli-table: 0.3.11 @@ -10948,26 +11018,26 @@ snapshots: - supports-color - typescript - '@lingui/conf@5.9.5(typescript@6.0.3)': + '@lingui/conf@5.9.5(typescript@7.0.2)': dependencies: '@babel/runtime': 7.29.2 - cosmiconfig: 8.3.6(typescript@6.0.3) + cosmiconfig: 8.3.6(typescript@7.0.2) jest-validate: 29.7.0 jiti: 2.7.0 picocolors: 1.1.1 transitivePeerDependencies: - typescript - '@lingui/core@5.9.5(@lingui/babel-plugin-lingui-macro@5.9.5(typescript@6.0.3))': + '@lingui/core@5.9.5(@lingui/babel-plugin-lingui-macro@5.9.5(typescript@7.0.2))': dependencies: '@babel/runtime': 7.29.2 '@lingui/message-utils': 5.9.5 optionalDependencies: - '@lingui/babel-plugin-lingui-macro': 5.9.5(typescript@6.0.3) + '@lingui/babel-plugin-lingui-macro': 5.9.5(typescript@7.0.2) - '@lingui/format-po@5.9.5(typescript@6.0.3)': + '@lingui/format-po@5.9.5(typescript@7.0.2)': dependencies: - '@lingui/conf': 5.9.5(typescript@6.0.3) + '@lingui/conf': 5.9.5(typescript@7.0.2) '@lingui/message-utils': 5.9.5 date-fns: 3.6.0 pofile: 1.1.4 @@ -10979,13 +11049,13 @@ snapshots: '@messageformat/parser': 5.1.1 js-sha256: 0.10.1 - '@lingui/react@5.9.5(@lingui/babel-plugin-lingui-macro@5.9.5(typescript@6.0.3))(react@19.1.0)': + '@lingui/react@5.9.5(@lingui/babel-plugin-lingui-macro@5.9.5(typescript@7.0.2))(react@19.1.0)': dependencies: '@babel/runtime': 7.29.2 - '@lingui/core': 5.9.5(@lingui/babel-plugin-lingui-macro@5.9.5(typescript@6.0.3)) + '@lingui/core': 5.9.5(@lingui/babel-plugin-lingui-macro@5.9.5(typescript@7.0.2)) react: 19.1.0 optionalDependencies: - '@lingui/babel-plugin-lingui-macro': 5.9.5(typescript@6.0.3) + '@lingui/babel-plugin-lingui-macro': 5.9.5(typescript@7.0.2) '@messageformat/parser@5.1.1': dependencies: @@ -12563,36 +12633,65 @@ snapshots: dependencies: '@types/yargs-parser': 21.0.3 - '@typescript/native-preview-darwin-arm64@7.0.0-dev.20260512.1': + '@typescript/typescript-aix-ppc64@7.0.2': optional: true - '@typescript/native-preview-darwin-x64@7.0.0-dev.20260512.1': + '@typescript/typescript-darwin-arm64@7.0.2': optional: true - '@typescript/native-preview-linux-arm64@7.0.0-dev.20260512.1': + '@typescript/typescript-darwin-x64@7.0.2': optional: true - '@typescript/native-preview-linux-arm@7.0.0-dev.20260512.1': + '@typescript/typescript-freebsd-arm64@7.0.2': optional: true - '@typescript/native-preview-linux-x64@7.0.0-dev.20260512.1': + '@typescript/typescript-freebsd-x64@7.0.2': optional: true - '@typescript/native-preview-win32-arm64@7.0.0-dev.20260512.1': + '@typescript/typescript-linux-arm64@7.0.2': optional: true - '@typescript/native-preview-win32-x64@7.0.0-dev.20260512.1': + '@typescript/typescript-linux-arm@7.0.2': optional: true - '@typescript/native-preview@7.0.0-dev.20260512.1': - optionalDependencies: - '@typescript/native-preview-darwin-arm64': 7.0.0-dev.20260512.1 - '@typescript/native-preview-darwin-x64': 7.0.0-dev.20260512.1 - '@typescript/native-preview-linux-arm': 7.0.0-dev.20260512.1 - '@typescript/native-preview-linux-arm64': 7.0.0-dev.20260512.1 - '@typescript/native-preview-linux-x64': 7.0.0-dev.20260512.1 - '@typescript/native-preview-win32-arm64': 7.0.0-dev.20260512.1 - '@typescript/native-preview-win32-x64': 7.0.0-dev.20260512.1 + '@typescript/typescript-linux-loong64@7.0.2': + optional: true + + '@typescript/typescript-linux-mips64el@7.0.2': + optional: true + + '@typescript/typescript-linux-ppc64@7.0.2': + optional: true + + '@typescript/typescript-linux-riscv64@7.0.2': + optional: true + + '@typescript/typescript-linux-s390x@7.0.2': + optional: true + + '@typescript/typescript-linux-x64@7.0.2': + optional: true + + '@typescript/typescript-netbsd-arm64@7.0.2': + optional: true + + '@typescript/typescript-netbsd-x64@7.0.2': + optional: true + + '@typescript/typescript-openbsd-arm64@7.0.2': + optional: true + + '@typescript/typescript-openbsd-x64@7.0.2': + optional: true + + '@typescript/typescript-sunos-x64@7.0.2': + optional: true + + '@typescript/typescript-win32-arm64@7.0.2': + optional: true + + '@typescript/typescript-win32-x64@7.0.2': + optional: true '@ungap/structured-clone@1.3.1': {} @@ -13432,14 +13531,14 @@ snapshots: core-util-is@1.0.3: {} - cosmiconfig@8.3.6(typescript@6.0.3): + cosmiconfig@8.3.6(typescript@7.0.2): dependencies: import-fresh: 3.3.1 js-yaml: 4.1.1 parse-json: 5.2.0 path-type: 4.0.0 optionalDependencies: - typescript: 6.0.3 + typescript: 7.0.2 create-jest@29.7.0(@types/node@24.12.4): dependencies: @@ -18106,7 +18205,28 @@ snapshots: possible-typed-array-names: 1.1.0 reflect.getprototypeof: 1.0.10 - typescript@6.0.3: {} + typescript@7.0.2: + optionalDependencies: + '@typescript/typescript-aix-ppc64': 7.0.2 + '@typescript/typescript-darwin-arm64': 7.0.2 + '@typescript/typescript-darwin-x64': 7.0.2 + '@typescript/typescript-freebsd-arm64': 7.0.2 + '@typescript/typescript-freebsd-x64': 7.0.2 + '@typescript/typescript-linux-arm': 7.0.2 + '@typescript/typescript-linux-arm64': 7.0.2 + '@typescript/typescript-linux-loong64': 7.0.2 + '@typescript/typescript-linux-mips64el': 7.0.2 + '@typescript/typescript-linux-ppc64': 7.0.2 + '@typescript/typescript-linux-riscv64': 7.0.2 + '@typescript/typescript-linux-s390x': 7.0.2 + '@typescript/typescript-linux-x64': 7.0.2 + '@typescript/typescript-netbsd-arm64': 7.0.2 + '@typescript/typescript-netbsd-x64': 7.0.2 + '@typescript/typescript-openbsd-arm64': 7.0.2 + '@typescript/typescript-openbsd-x64': 7.0.2 + '@typescript/typescript-sunos-x64': 7.0.2 + '@typescript/typescript-win32-arm64': 7.0.2 + '@typescript/typescript-win32-x64': 7.0.2 ua-parser-js@0.7.41: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index c917be629a..f916bbe6a3 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -43,3 +43,25 @@ patchedDependencies: minimumReleaseAgeExclude: - '@atproto/*' - '@bsky.app/*' + # TODO: remove once these are old enough + - 'typescript' + - '@typescript/typescript-aix-ppc64@7.0.2' + - '@typescript/typescript-darwin-arm64@7.0.2' + - '@typescript/typescript-darwin-x64@7.0.2' + - '@typescript/typescript-freebsd-arm64@7.0.2' + - '@typescript/typescript-freebsd-x64@7.0.2' + - '@typescript/typescript-linux-arm64@7.0.2' + - '@typescript/typescript-linux-arm@7.0.2' + - '@typescript/typescript-linux-loong64@7.0.2' + - '@typescript/typescript-linux-mips64el@7.0.2' + - '@typescript/typescript-linux-ppc64@7.0.2' + - '@typescript/typescript-linux-riscv64@7.0.2' + - '@typescript/typescript-linux-s390x@7.0.2' + - '@typescript/typescript-linux-x64@7.0.2' + - '@typescript/typescript-netbsd-arm64@7.0.2' + - '@typescript/typescript-netbsd-x64@7.0.2' + - '@typescript/typescript-openbsd-arm64@7.0.2' + - '@typescript/typescript-openbsd-x64@7.0.2' + - '@typescript/typescript-sunos-x64@7.0.2' + - '@typescript/typescript-win32-arm64@7.0.2' + - '@typescript/typescript-win32-x64@7.0.2' From 4778142ee8c3f3dccf3fad14abcda3c31ab59dd9 Mon Sep 17 00:00:00 2001 From: DS Boyce <260543580+ds-boyce@users.noreply.github.com> Date: Tue, 14 Jul 2026 05:57:11 -0700 Subject: [PATCH 23/52] Create primary tooltip variant (#11140) --- oxlint-suppressions.json | 10 --- src/components/Tooltip/const.ts | 37 +++++++- src/components/Tooltip/index.e2e.tsx | 2 +- src/components/Tooltip/index.tsx | 86 ++++++++++--------- src/components/Tooltip/index.web.tsx | 63 ++++++++------ .../SubscribeProfileButton.tsx | 18 ++-- .../com/composer/threadgate/ThreadgateBtn.tsx | 26 +++--- src/view/screens/Storybook/Storybook.tsx | 2 + src/view/screens/Storybook/Tooltips.tsx | 55 ++++++++++++ 9 files changed, 190 insertions(+), 109 deletions(-) create mode 100644 src/view/screens/Storybook/Tooltips.tsx diff --git a/oxlint-suppressions.json b/oxlint-suppressions.json index 685e7144c3..21dd9f702b 100644 --- a/oxlint-suppressions.json +++ b/oxlint-suppressions.json @@ -406,16 +406,6 @@ "count": 1 } }, - "src/components/Tooltip/index.tsx": { - "typescript/no-base-to-string": { - "count": 1 - } - }, - "src/components/Tooltip/index.web.tsx": { - "typescript/no-base-to-string": { - "count": 1 - } - }, "src/components/WhoCanReply.tsx": { "typescript/no-floating-promises": { "count": 1 diff --git a/src/components/Tooltip/const.ts b/src/components/Tooltip/const.ts index cad7e0106c..eaa24f24a3 100644 --- a/src/components/Tooltip/const.ts +++ b/src/components/Tooltip/const.ts @@ -1,6 +1,41 @@ -import {atoms as a} from '#/alf' +import {atoms as a, select, type Theme} from '#/alf' + +/** + * Visual variant for the tooltip surface. `default` is a neutral floating card; + * `primary` is a subtle blue surface matching the `primary_subtle` button. + */ +export type TooltipColor = 'default' | 'primary' export const BUBBLE_MAX_WIDTH = 240 export const ARROW_SIZE = 12 export const ARROW_HALF_SIZE = ARROW_SIZE / 2 export const MIN_EDGE_SPACE = a.px_lg.paddingLeft + +/** + * Resolves the surface (background/arrow fill) and text colors for a tooltip + * variant. Kept here so the native and web implementations stay in sync. + */ +export function getTooltipStyle(t: Theme, color: TooltipColor) { + if (color === 'primary') { + return { + surface: t.palette.primary_50, + text: t.palette.primary_600, + border: { + color: t.atoms.border_contrast_low.borderColor, + width: 1, + }, + } + } + return { + surface: select(t.name, { + light: t.atoms.bg.backgroundColor, + dark: t.atoms.bg_contrast_100.backgroundColor, + dim: t.atoms.bg_contrast_100.backgroundColor, + }), + text: t.atoms.text.color, + border: { + color: undefined, + width: 0, + }, + } +} diff --git a/src/components/Tooltip/index.e2e.tsx b/src/components/Tooltip/index.e2e.tsx index 905da174c1..ae8a9c4896 100644 --- a/src/components/Tooltip/index.e2e.tsx +++ b/src/components/Tooltip/index.e2e.tsx @@ -14,6 +14,6 @@ export function Content() { return null } -export function TextBubble() { +export function BubbleText() { return null } diff --git a/src/components/Tooltip/index.tsx b/src/components/Tooltip/index.tsx index e916ee0ed3..acabffc267 100644 --- a/src/components/Tooltip/index.tsx +++ b/src/components/Tooltip/index.tsx @@ -1,5 +1,4 @@ import { - Children, createContext, useCallback, useContext, @@ -14,14 +13,16 @@ import {useSafeAreaInsets} from 'react-native-safe-area-context' import {useIsKeyboardVisible} from '#/lib/hooks/useIsKeyboardVisible' import {GlobalGestureEventsProvider} from '#/state/global-gesture-events' -import {atoms as a, select, useTheme} from '#/alf' +import {atoms as a, useTheme} from '#/alf' import {useOnGesture} from '#/components/hooks/useOnGesture' import {createPortalGroup, Portal as RootPortal} from '#/components/Portal' import { ARROW_HALF_SIZE, ARROW_SIZE, BUBBLE_MAX_WIDTH, + getTooltipStyle, MIN_EDGE_SPACE, + type TooltipColor, } from '#/components/Tooltip/const' import {Text} from '#/components/Typography' @@ -56,10 +57,10 @@ SheetCompatProvider.displayName = 'TooltipSheetCompatProvider' * These are native specific values, not shared with web */ const ARROW_VISUAL_OFFSET = ARROW_SIZE / 1.25 // vibes-based, slightly off the target -const BUBBLE_SHADOW_OFFSET = ARROW_SIZE / 3 // vibes-based, provide more shadow beneath tip type TooltipContextType = { position: 'top' | 'bottom' + color: TooltipColor visible: boolean onVisibleChange: (visible: boolean) => void } @@ -79,6 +80,7 @@ type TargetContextType = { const TooltipContext = createContext({ position: 'bottom', + color: 'default', visible: false, onVisibleChange: () => {}, }) @@ -94,11 +96,13 @@ TargetContext.displayName = 'TargetContext' export function Outer({ children, position = 'bottom', + color = 'default', visible: requestVisible, onVisibleChange, }: { children: React.ReactNode position?: 'top' | 'bottom' + color?: TooltipColor visible: boolean onVisibleChange: (visible: boolean) => void }) { @@ -126,8 +130,8 @@ export function Outer({ } const ctx = useMemo( - () => ({position, visible, onVisibleChange}), - [position, visible, onVisibleChange], + () => ({position, color, visible, onVisibleChange}), + [position, color, visible, onVisibleChange], ) const targetCtx = useMemo( () => ({ @@ -149,13 +153,13 @@ export function Outer({ export function Target({children}: {children: React.ReactNode}) { const {shouldMeasure, setTargetMeasurements} = useContext(TargetContext) - const [hasLayedOut, setHasLayedOut] = useState(false) + const [hasLaidOut, setHasLaidOut] = useState(false) const targetRef = useRef(null) const containerRef = useContext(TooltipProviderContext) const keyboardIsOpen = useIsKeyboardVisible() useEffect(() => { - if (!shouldMeasure || !hasLayedOut) return + if (!shouldMeasure || !hasLaidOut) return /* * Once opened, measure the dimensions and position of the target */ @@ -179,7 +183,7 @@ export function Target({children}: {children: React.ReactNode}) { }, [ shouldMeasure, setTargetMeasurements, - hasLayedOut, + hasLaidOut, containerRef, keyboardIsOpen, ]) @@ -188,7 +192,7 @@ export function Target({children}: {children: React.ReactNode}) { setHasLayedOut(true)}> + onLayout={() => setHasLaidOut(true)}> {children} ) @@ -201,7 +205,7 @@ export function Content({ children: React.ReactNode label: string }) { - const {position, visible, onVisibleChange} = useContext(TooltipContext) + const {position, color, visible, onVisibleChange} = useContext(TooltipContext) const {targetMeasurements} = useContext(TargetContext) const isWithinProvider = !!useContext(TooltipProviderContext) const requestClose = useCallback(() => { @@ -217,8 +221,9 @@ export function Content({ void targetMeasurements: Exclude< TargetContextType['targetMeasurements'], @@ -246,6 +253,7 @@ function Bubble({ > }) { const t = useTheme() + const style = getTooltipStyle(t, color) const insets = useSafeAreaInsets() const dimensions = useWindowDimensions() const [bubbleMeasurements, setBubbleMeasurements] = useState< @@ -383,21 +391,19 @@ function Bubble({ ]}> + style={{transformOrigin: opposite(position)}}> { @@ -440,7 +437,7 @@ function Bubble({ ) } -function oppposite(position: 'top' | 'bottom') { +function opposite(position: 'top' | 'bottom') { switch (position) { case 'top': return 'center bottom' @@ -451,16 +448,23 @@ function oppposite(position: 'top' | 'bottom') { } } -export function TextBubble({children}: {children: React.ReactNode}) { - const c = Children.toArray(children) +export function BubbleText({ + children, + label, +}: { + children: React.ReactNode + label: string +}) { + const t = useTheme() + const {color} = useContext(TooltipContext) + const style = getTooltipStyle(t, color) + // eslint-disable-next-line bsky-internal/avoid-unwrapped-text return ( - + - {c.map((child, i) => ( - - {child} - - ))} + + {children} + ) diff --git a/src/components/Tooltip/index.web.tsx b/src/components/Tooltip/index.web.tsx index 9d36975aaa..fd017b43ca 100644 --- a/src/components/Tooltip/index.web.tsx +++ b/src/components/Tooltip/index.web.tsx @@ -1,13 +1,15 @@ -import {Children, createContext, useContext, useMemo} from 'react' +import {createContext, useContext, useMemo} from 'react' import {View} from 'react-native' import {utils} from '@bsky.app/alf' import {Popover} from 'radix-ui' -import {atoms as a, flatten, select, useTheme} from '#/alf' +import {atoms as a, flatten, useTheme} from '#/alf' import { ARROW_SIZE, BUBBLE_MAX_WIDTH, + getTooltipStyle, MIN_EDGE_SPACE, + type TooltipColor, } from '#/components/Tooltip/const' import {Text} from '#/components/Typography' @@ -19,26 +21,32 @@ Provider.displayName = 'TooltipProvider' type TooltipContextType = { position: 'top' | 'bottom' + color: TooltipColor onVisibleChange: (open: boolean) => void } -const TooltipContext = createContext>({ +const TooltipContext = createContext< + Pick +>({ position: 'bottom', + color: 'default', }) TooltipContext.displayName = 'TooltipContext' export function Outer({ children, position = 'bottom', + color = 'default', visible, onVisibleChange, }: { children: React.ReactNode position?: 'top' | 'bottom' + color?: TooltipColor visible: boolean onVisibleChange: (visible: boolean) => void }) { - const ctx = useMemo(() => ({position}), [position]) + const ctx = useMemo(() => ({position, color}), [position, color]) return ( {children} @@ -62,7 +70,8 @@ export function Content({ label: string }) { const t = useTheme() - const {position} = useContext(TooltipContext) + const {position, color} = useContext(TooltipContext) + const style = getTooltipStyle(t, color) return ( {children} @@ -109,16 +109,23 @@ export function Content({ ) } -export function TextBubble({children}: {children: React.ReactNode}) { - const c = Children.toArray(children) +export function BubbleText({ + children, + label, +}: { + children: React.ReactNode + label: string +}) { + const t = useTheme() + const {color} = useContext(TooltipContext) + const style = getTooltipStyle(t, color) + // eslint-disable-next-line bsky-internal/avoid-unwrapped-text return ( - + - {c.map((child, i) => ( - - {child} - - ))} + + {children} + ) diff --git a/src/components/activity-notifications/SubscribeProfileButton.tsx b/src/components/activity-notifications/SubscribeProfileButton.tsx index 7bbc677d47..ce9dcd05ae 100644 --- a/src/components/activity-notifications/SubscribeProfileButton.tsx +++ b/src/components/activity-notifications/SubscribeProfileButton.tsx @@ -1,8 +1,6 @@ import {useCallback, useEffect, useState} from 'react' import {type ModerationOpts} from '@atproto/api' -import {msg} from '@lingui/core/macro' -import {useLingui} from '@lingui/react' -import {Trans} from '@lingui/react/macro' +import {Trans, useLingui} from '@lingui/react/macro' import {useRequireEmailVerification} from '#/lib/hooks/useRequireEmailVerification' import {createSanitizedDisplayName} from '#/lib/moderation/create-sanitized-display-name' @@ -11,7 +9,6 @@ import {useDialogControl} from '#/components/Dialog' import {BellPlus_Stroke2_Corner0_Rounded as BellPlusIcon} from '#/components/icons/BellPlus' import {BellRinging_Filled_Corner0_Rounded as BellRingingIcon} from '#/components/icons/BellRinging' import * as Tooltip from '#/components/Tooltip' -import {Text} from '#/components/Typography' import {useActivitySubscriptionsNudged} from '#/storage/hooks/activity-subscriptions-nudged' import type * as bsky from '#/types/bsky' import {SubscribeProfileDialog} from './SubscribeProfileDialog' @@ -25,7 +22,7 @@ export function SubscribeProfileButton({ moderationOpts: ModerationOpts disableHint?: boolean }) { - const {_} = useLingui() + const {t: l} = useLingui() const requireEmailVerification = useRequireEmailVerification() const subscribeDialogControl = useDialogControl() const [activitySubscriptionsNudged, setActivitySubscriptionsNudged] = @@ -84,18 +81,15 @@ export function SubscribeProfileButton({ size="small" color={tooltipVisible ? 'primary_subtle' : 'secondary'} shape="round" - label={_(msg`Get notified when ${name} posts`)} + label={l`Get notified when ${name} posts`} onPress={wrappedOnPress}> - - - Get notified about new posts - - + + Get notified about new posts + - > }) { - const {_} = useLingui() + const {t: l} = useLingui() const ax = useAnalytics() const control = Dialog.useDialogControl() const [threadgateNudged, setThreadgateNudged] = useThreadgateNudged() const [showTooltip, setShowTooltip] = useState(false) + // eslint-disable-next-line react/hook-use-state const [tooltipWasShown] = useState(!threadgateNudged) useEffect(() => { @@ -134,8 +132,8 @@ export function ThreadgateBtn({ !postgate.embeddingRules || postgate.embeddingRules.length === 0 const anyoneCanInteract = anyoneCanReply && anyoneCanQuote const label = anyoneCanInteract - ? _(msg`Anyone can interact`) - : _(msg`Interaction limited`) + ? l`Anyone can interact` + : l`Interaction limited` return ( <> @@ -150,9 +148,7 @@ export function ThreadgateBtn({ testID="openReplyGateButton" onPress={onPress} label={label} - accessibilityHint={_( - msg`Opens a dialog to choose who can interact with this post`, - )}> + accessibilityHint={l`Opens a dialog to choose who can interact with this post`}> {label} @@ -160,13 +156,11 @@ export function ThreadgateBtn({ - - - Psst! You can edit who can interact with this post. - - + + Psst! You can edit who can interact with this post. + - { diff --git a/src/view/screens/Storybook/Storybook.tsx b/src/view/screens/Storybook/Storybook.tsx index cc11332126..1583e923f5 100644 --- a/src/view/screens/Storybook/Storybook.tsx +++ b/src/view/screens/Storybook/Storybook.tsx @@ -26,6 +26,7 @@ import {Shadows} from './Shadows' import {Spacing} from './Spacing' import {Theming} from './Theming' import {Toasts} from './Toasts' +import {Tooltips} from './Tooltips' import {Typography} from './Typography' export default function Storybook() { @@ -127,6 +128,7 @@ export default function Storybook() { + diff --git a/src/view/screens/Storybook/Tooltips.tsx b/src/view/screens/Storybook/Tooltips.tsx new file mode 100644 index 0000000000..d9955ec9d9 --- /dev/null +++ b/src/view/screens/Storybook/Tooltips.tsx @@ -0,0 +1,55 @@ +import {useState} from 'react' +import {View} from 'react-native' + +import {atoms as a} from '#/alf' +import {Button, ButtonText} from '#/components/Button' +import * as Tooltip from '#/components/Tooltip' +import {H1} from '#/components/Typography' + +export function Tooltips() { + const [defaultVisible, setDefaultVisible] = useState(false) + const [primaryVisible, setPrimaryVisible] = useState(false) + + return ( + +

Tooltips

+ + + + + + + + This is a default tooltip. + + + + + + + + + This is a primary tooltip. + + + +
+ ) +} From 30e862bc26bb3f6120874c8debddd3e5b9d7c5a1 Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Tue, 14 Jul 2026 16:14:11 +0300 Subject: [PATCH 24/52] Allow up to 10 images in share intent (#11132) --- modules/Share-with-Bluesky/ShareViewController.swift | 12 ++++-------- .../ExpoReceiveAndroidIntentsModule.kt | 5 +++-- 2 files changed, 7 insertions(+), 10 deletions(-) diff --git a/modules/Share-with-Bluesky/ShareViewController.swift b/modules/Share-with-Bluesky/ShareViewController.swift index 4eeab8876b..b31f671772 100644 --- a/modules/Share-with-Bluesky/ShareViewController.swift +++ b/modules/Share-with-Bluesky/ShareViewController.swift @@ -3,6 +3,7 @@ import AVKit let IMAGE_EXTENSIONS: [String] = ["png", "jpg", "jpeg", "gif", "heic"] let MOVIE_EXTENSIONS: [String] = ["mov", "mp4", "m4v"] +let MAX_IMAGES = 10 enum URLType: String, CaseIterable { case image @@ -71,17 +72,12 @@ class ShareViewController: UIViewController { } private func handleImages(items: [NSItemProvider]) async { - let firstFourItems: [NSItemProvider] - if items.count < 4 { - firstFourItems = items - } else { - firstFourItems = Array(items[0...3]) - } + let itemsToProcess = Array(items.prefix(MAX_IMAGES)) var valid = true var imageUris = "" - for (index, item) in firstFourItems.enumerated() { + for (index, item) in itemsToProcess.enumerated() { var imageUriInfo: String? do { @@ -100,7 +96,7 @@ class ShareViewController: UIViewController { if let imageUriInfo = imageUriInfo { imageUris.append(imageUriInfo) - if index < items.count - 1 { + if index < itemsToProcess.count - 1 { imageUris.append(",") } } else { diff --git a/modules/expo-receive-android-intents/android/src/main/java/xyz/blueskyweb/app/exporeceiveandroidintents/ExpoReceiveAndroidIntentsModule.kt b/modules/expo-receive-android-intents/android/src/main/java/xyz/blueskyweb/app/exporeceiveandroidintents/ExpoReceiveAndroidIntentsModule.kt index af7344216b..8b4784b04e 100644 --- a/modules/expo-receive-android-intents/android/src/main/java/xyz/blueskyweb/app/exporeceiveandroidintents/ExpoReceiveAndroidIntentsModule.kt +++ b/modules/expo-receive-android-intents/android/src/main/java/xyz/blueskyweb/app/exporeceiveandroidintents/ExpoReceiveAndroidIntentsModule.kt @@ -15,6 +15,7 @@ import java.io.FileOutputStream import java.net.URLEncoder private const val TAG = "ExpoReceiveAndroidIntents" +private const val MAX_IMAGES = 10 enum class AttachmentType { IMAGE, @@ -100,12 +101,12 @@ class ExpoReceiveAndroidIntentsModule : Module() { intent .getParcelableArrayListExtra(Intent.EXTRA_STREAM, Uri::class.java) ?.filterIsInstance() - ?.take(4) + ?.take(MAX_IMAGES) } else { intent .getParcelableArrayListExtra(Intent.EXTRA_STREAM) ?.filterIsInstance() - ?.take(4) + ?.take(MAX_IMAGES) } val text = intent.getStringExtra(Intent.EXTRA_TEXT) From a5adb0e97a30116bcb1e3771c677128895eedf13 Mon Sep 17 00:00:00 2001 From: Spence Pope Date: Tue, 14 Jul 2026 10:38:39 -0400 Subject: [PATCH 25/52] Add video playback observability to Sentry (#11111) --- .../VideoEmbedInner/VideoEmbedInnerNative.tsx | 7 + .../Post/Embed/VideoEmbed/index.tsx | 47 ++++++- .../video/__tests__/playbackTelemetry.test.ts | 120 ++++++++++++++++++ src/lib/media/video/playbackTelemetry.ts | 83 ++++++++++++ src/logger/sentry/setup/index.ts | 17 ++- src/screens/VideoFeed/index.tsx | 56 +++++++- 6 files changed, 324 insertions(+), 6 deletions(-) create mode 100644 src/lib/media/video/__tests__/playbackTelemetry.test.ts create mode 100644 src/lib/media/video/playbackTelemetry.ts diff --git a/src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerNative.tsx b/src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerNative.tsx index f38677ea1d..bead58fd91 100644 --- a/src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerNative.tsx +++ b/src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerNative.tsx @@ -26,12 +26,18 @@ export function VideoEmbedInnerNative({ setStatus, setIsLoading, setIsActive, + onError, }: { ref: React.Ref<{togglePlayback: () => void}> embed: AppBskyEmbedVideo.View setStatus: (status: 'playing' | 'paused') => void setIsLoading: (isLoading: boolean) => void setIsActive: (isActive: boolean) => void + /** + * Called with the native error message before the component throws to the + * surrounding error boundary. + */ + onError?: (error: string) => void }) { const {_} = useLingui() const videoRef = useRef(null) @@ -81,6 +87,7 @@ export function VideoEmbedInnerNative({ setTimeRemaining(e.nativeEvent.timeRemaining) }} onError={e => { + onError?.(e.nativeEvent.error) setError(e.nativeEvent.error) }} ref={videoRef} diff --git a/src/components/Post/Embed/VideoEmbed/index.tsx b/src/components/Post/Embed/VideoEmbed/index.tsx index afe1bcedb6..66693e780c 100644 --- a/src/components/Post/Embed/VideoEmbed/index.tsx +++ b/src/components/Post/Embed/VideoEmbed/index.tsx @@ -1,4 +1,4 @@ -import {useCallback, useRef, useState} from 'react' +import {useCallback, useEffect, useRef, useState} from 'react' import {ActivityIndicator, View} from 'react-native' import {ImageBackground} from 'expo-image' import {type AppBskyEmbedVideo} from '@atproto/api' @@ -6,6 +6,10 @@ import {msg} from '@lingui/core/macro' import {useLingui} from '@lingui/react' import {Trans} from '@lingui/react/macro' +import { + createPlaybackTelemetry, + type PlaybackTelemetry, +} from '#/lib/media/video/playbackTelemetry' import {ErrorBoundary} from '#/view/com/util/ErrorBoundary' import {atoms as a, platform} from '#/alf' import {Button} from '#/components/Button' @@ -75,6 +79,17 @@ function InnerWrapper({embed}: Props) { const [isActive, setIsActive] = useState(false) const showSpinner = useThrottledValue(isActive && isLoading, 100) + /* + * Created lazily on first activation so videos that are never scrolled into + * the active position cost nothing. + */ + const telemetryRef = useRef(null) + useEffect(() => { + return () => { + telemetryRef.current?.deactivated() + } + }, []) + const showOverlay = !isActive || isLoading || @@ -89,9 +104,33 @@ function InnerWrapper({embed}: Props) { <> { + setStatus(s) + if (s === 'playing') { + telemetryRef.current?.playing() + } + }} + setIsLoading={loading => { + setIsLoading(loading) + if (!loading) { + telemetryRef.current?.ready() + } + }} + setIsActive={active => { + setIsActive(active) + if (active) { + telemetryRef.current ??= createPlaybackTelemetry({ + surface: 'feed', + presentation: embed.presentation === 'gif' ? 'gif' : 'video', + }) + telemetryRef.current.activated() + } else { + telemetryRef.current?.deactivated() + } + }} + onError={error => { + telemetryRef.current?.error(error) + }} ref={ref} /> ({ + Sentry: { + startInactiveSpan: jest.fn(() => mockSpan), + }, +})) + +const {Sentry}: {Sentry: {startInactiveSpan: jest.Mock}} = jest.requireMock( + '#/logger/sentry/lib', +) + +describe('createPlaybackTelemetry', () => { + beforeEach(() => { + jest.clearAllMocks() + }) + + it('starts a root span on activation with surface and presentation', () => { + const telemetry = createPlaybackTelemetry({ + surface: 'feed', + presentation: 'video', + }) + telemetry.activated() + expect(Sentry.startInactiveSpan).toHaveBeenCalledWith({ + name: 'video.playback', + op: 'video.playback', + attributes: {surface: 'feed', presentation: 'video'}, + }) + }) + + it('does nothing before activation', () => { + const telemetry = createPlaybackTelemetry({ + surface: 'feed', + presentation: 'video', + }) + telemetry.ready() + telemetry.playing() + telemetry.deactivated() + expect(Sentry.startInactiveSpan).not.toHaveBeenCalled() + expect(mockSpan.end).not.toHaveBeenCalled() + }) + + it('ignores duplicate activations while a span is open', () => { + const telemetry = createPlaybackTelemetry({ + surface: 'feed', + presentation: 'video', + }) + telemetry.activated() + telemetry.activated() + expect(Sentry.startInactiveSpan).toHaveBeenCalledTimes(1) + }) + + it('records ready and playing times once', () => { + const telemetry = createPlaybackTelemetry({ + surface: 'feed', + presentation: 'video', + }) + telemetry.activated() + telemetry.ready() + telemetry.ready() + telemetry.playing() + telemetry.playing() + const calls = mockSpan.setAttribute.mock.calls as [string, unknown][] + const attrs = calls.map(c => c[0]) + expect(attrs.filter(a => a === 'timeToReadyMs')).toHaveLength(1) + expect(attrs.filter(a => a === 'timeToFirstPlayMs')).toHaveLength(1) + }) + + it('ends the span with ok outcome on deactivation, idempotently', () => { + const telemetry = createPlaybackTelemetry({ + surface: 'feed', + presentation: 'video', + }) + telemetry.activated() + telemetry.deactivated() + telemetry.deactivated() + expect(mockSpan.setAttribute).toHaveBeenCalledWith('outcome', 'ok') + expect(mockSpan.end).toHaveBeenCalledTimes(1) + }) + + it('ends the span with error outcome and message', () => { + const telemetry = createPlaybackTelemetry({ + surface: 'feed', + presentation: 'video', + }) + telemetry.activated() + telemetry.error('AVFoundationErrorDomain -11850') + expect(mockSpan.setAttribute).toHaveBeenCalledWith( + 'errorMessage', + 'AVFoundationErrorDomain -11850', + ) + expect(mockSpan.setAttribute).toHaveBeenCalledWith('outcome', 'error') + expect(mockSpan.end).toHaveBeenCalledTimes(1) + }) + + it('starts a new span for a new activation window', () => { + const telemetry = createPlaybackTelemetry({ + surface: 'immersiveFeed', + presentation: 'video', + }) + telemetry.activated({preloaded: true}) + telemetry.deactivated() + telemetry.activated({preloaded: false}) + expect(Sentry.startInactiveSpan).toHaveBeenCalledTimes(2) + expect(Sentry.startInactiveSpan).toHaveBeenLastCalledWith({ + name: 'video.playback', + op: 'video.playback', + attributes: { + surface: 'immersiveFeed', + presentation: 'video', + preloaded: false, + }, + }) + }) +}) diff --git a/src/lib/media/video/playbackTelemetry.ts b/src/lib/media/video/playbackTelemetry.ts new file mode 100644 index 0000000000..d554619b85 --- /dev/null +++ b/src/lib/media/video/playbackTelemetry.ts @@ -0,0 +1,83 @@ +import {Sentry} from '#/logger/sentry/lib' + +/** + * Where the video is being played. + */ +export type PlaybackSurface = 'feed' | 'immersiveFeed' + +export type PlaybackTelemetry = { + activated: (opts?: {preloaded?: boolean}) => void + ready: () => void + playing: () => void + error: (e: unknown) => void + deactivated: () => void +} + +/** + * Sentry-only observability for video playback in feeds. Opens a root span + * per activation window (the video becomes the active, autoplaying one) and + * ends it on deactivation. Because these are root spans, the SDK attaches JS + * stall and slow/frozen frame measurements to them, capturing scroll + * smoothness while a video is on screen. + * + * Does not report to the analytics pipeline, only Sentry. + */ +export function createPlaybackTelemetry({ + surface, + presentation, +}: { + surface: PlaybackSurface + presentation: 'video' | 'gif' +}): PlaybackTelemetry { + let span: ReturnType | undefined + let activatedAt = 0 + let sawReady = false + let sawPlaying = false + + function end(outcome: 'ok' | 'error') { + if (!span) return + span.setAttribute('outcome', outcome) + span.end() + span = undefined + } + + return { + activated(opts) { + if (span) return + activatedAt = Date.now() + sawReady = false + sawPlaying = false + span = Sentry.startInactiveSpan({ + name: 'video.playback', + op: 'video.playback', + attributes: { + surface, + presentation, + ...(opts?.preloaded !== undefined && {preloaded: opts.preloaded}), + }, + }) + }, + + ready() { + if (!span || sawReady) return + sawReady = true + span.setAttribute('timeToReadyMs', Date.now() - activatedAt) + }, + + playing() { + if (!span || sawPlaying) return + sawPlaying = true + span.setAttribute('timeToFirstPlayMs', Date.now() - activatedAt) + }, + + error(e) { + if (!span) return + span.setAttribute('errorMessage', String(e).slice(0, 256)) + end('error') + }, + + deactivated() { + end('ok') + }, + } +} diff --git a/src/logger/sentry/setup/index.ts b/src/logger/sentry/setup/index.ts index 666fc3b0a6..d4607e08a1 100644 --- a/src/logger/sentry/setup/index.ts +++ b/src/logger/sentry/setup/index.ts @@ -1,4 +1,4 @@ -import {init} from '@sentry/react-native' +import {getGlobalScope, init} from '@sentry/react-native' import * as env from '#/env' @@ -30,4 +30,19 @@ init({ */ attachStacktrace: false, sampleRate: env.IS_INTERNAL ? 1.0 : 0.1, + /** + * Sample rate for performance spans (video playback, video upload). Setting + * this also enables the SDK's default stall and slow/frozen frame tracking, + * whose measurements attach to every root span. + */ + tracesSampleRate: env.IS_INTERNAL ? 1.0 : 0.01, }) + +/* + * Events already carry react_native_context.fabric, but a tag is easier to + * filter and dashboard on. Detection matches the SDK's own isFabricEnabled. + */ +getGlobalScope().setTag( + 'new_arch', + (global as {nativeFabricUIManager?: unknown}).nativeFabricUIManager != null, +) diff --git a/src/screens/VideoFeed/index.tsx b/src/screens/VideoFeed/index.tsx index 7e760b88df..99dc03d208 100644 --- a/src/screens/VideoFeed/index.tsx +++ b/src/screens/VideoFeed/index.tsx @@ -47,6 +47,10 @@ import {HITSLOP_20} from '#/lib/constants' import {useHaptics} from '#/lib/haptics' import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback' import {useOpenComposer} from '#/lib/hooks/useOpenComposer' +import { + createPlaybackTelemetry, + type PlaybackTelemetry, +} from '#/lib/media/video/playbackTelemetry' import { type CommonNavigatorParams, type NavigationProp, @@ -551,7 +555,7 @@ let VideoItem = ({ <> {shouldRenderVideo && player && ( - + )} {moderation && ( { if (IS_ANDROID && !isReady && evt.currentTime >= 0.05) { setIsReady(true) @@ -609,6 +617,52 @@ function VideoItemInner({ ) } +/** + * Opens a Sentry playback span while this item is the active video. Adjacent + * players are preloaded by updateVideoState, so record whether the player was + * already ready at activation to separate load time from swipe latency. + */ +function usePlaybackTelemetry({ + player, + active, +}: { + player: VideoPlayer + active: boolean +}) { + const telemetryRef = useRef(null) + + useEffect(() => { + if (!active) return + telemetryRef.current ??= createPlaybackTelemetry({ + surface: 'immersiveFeed', + presentation: 'video', + }) + const telemetry = telemetryRef.current + const preloaded = player.status === 'readyToPlay' + telemetry.activated({preloaded}) + if (preloaded) { + telemetry.ready() + } + return () => { + telemetry.deactivated() + } + }, [active, player]) + + useEventListener(player, 'statusChange', evt => { + if (evt.status === 'readyToPlay') { + telemetryRef.current?.ready() + } else if (evt.status === 'error') { + telemetryRef.current?.error(evt.error?.message ?? 'unknown') + } + }) + + useEventListener(player, 'playingChange', evt => { + if (evt.isPlaying) { + telemetryRef.current?.playing() + } + }) +} + function ModerationOverlay({ embed, onPressShow, From 9fa5f15baa5ece3a398fbdc5ed7f6ac69884bdbd Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Tue, 14 Jul 2026 18:50:06 +0300 Subject: [PATCH 26/52] Revert "Typescript 7" (#11152) --- package.json | 5 +- pnpm-lock.yaml | 254 ++++++++++++-------------------------------- pnpm-workspace.yaml | 22 ---- 3 files changed, 70 insertions(+), 211 deletions(-) diff --git a/package.json b/package.json index 5fbbbf2f54..3dd3663a32 100644 --- a/package.json +++ b/package.json @@ -62,7 +62,7 @@ "lint": "oxlint --quiet src modules", "lint-native": "swiftlint ./modules && ktlint ./modules", "lint-native:fix": "swiftlint --fix ./modules && ktlint --format ./modules", - "typecheck": "tsc --project ./tsconfig.check.json", + "typecheck": "tsgo --project ./tsconfig.check.json", "e2e:mock-server": "cd dev-env && pnpm start", "e2e:build": "EXPO_PUBLIC_ENV=e2e NODE_ENV=test RN_SRC_EXT=e2e.ts,e2e.tsx expo run:ios", "e2e:build-android": "EXPO_PUBLIC_ENV=e2e NODE_ENV=test RN_SRC_EXT=e2e.ts,e2e.tsx expo run:android", @@ -270,6 +270,7 @@ "@types/psl": "1.1.1", "@types/react": "^19.1.17", "@types/react-dom": "^19.1.11", + "@typescript/native-preview": "^7.0.0-dev.20260428.1", "babel-jest": "^29.7.0", "babel-plugin-module-resolver": "^5.0.2", "babel-plugin-react-compiler": "19.1.0-rc.3", @@ -294,7 +295,7 @@ "react-refresh": "^0.14.0", "svgo": "^3.3.2", "ts-plugin-sort-import-suggestions": "^1.0.4", - "typescript": "^7.0.2", + "typescript": "^6.0.2", "webpack-bundle-analyzer": "^4.10.1" }, "jest": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2d9099c437..70e5e2ccf1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -330,10 +330,10 @@ importers: version: 9.2.7 '@lingui/core': specifier: ^5.9.2 - version: 5.9.5(@lingui/babel-plugin-lingui-macro@5.9.5(typescript@7.0.2)) + version: 5.9.5(@lingui/babel-plugin-lingui-macro@5.9.5(typescript@6.0.3)) '@lingui/react': specifier: ^5.9.2 - version: 5.9.5(@lingui/babel-plugin-lingui-macro@5.9.5(typescript@7.0.2))(react@19.1.0) + version: 5.9.5(@lingui/babel-plugin-lingui-macro@5.9.5(typescript@6.0.3))(react@19.1.0) '@react-native-async-storage/async-storage': specifier: 2.2.0 version: 2.2.0(react-native@0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0)) @@ -727,10 +727,10 @@ importers: version: 4.14.2 '@lingui/babel-plugin-lingui-macro': specifier: ^5.9.2 - version: 5.9.5(typescript@7.0.2) + version: 5.9.5(typescript@6.0.3) '@lingui/cli': specifier: ^5.9.2 - version: 5.9.5(typescript@7.0.2) + version: 5.9.5(typescript@6.0.3) '@pmmmwh/react-refresh-webpack-plugin': specifier: ^0.5.15 version: 0.5.17(react-refresh@0.14.2)(type-fest@1.4.0)(webpack-dev-server@4.15.2(webpack@5.106.2(postcss@8.5.14)))(webpack@5.106.2(postcss@8.5.14)) @@ -767,6 +767,9 @@ importers: '@types/react-dom': specifier: ^19.1.11 version: 19.1.11(@types/react@19.1.17) + '@typescript/native-preview': + specifier: ^7.0.0-dev.20260428.1 + version: 7.0.0-dev.20260512.1 babel-jest: specifier: ^29.7.0 version: 29.7.0(@babel/core@7.29.0) @@ -843,8 +846,8 @@ importers: specifier: ^1.0.4 version: 1.0.4 typescript: - specifier: ^7.0.2 - version: 7.0.2 + specifier: ^6.0.2 + version: 6.0.3 webpack-bundle-analyzer: specifier: ^4.10.1 version: 4.10.2 @@ -3712,126 +3715,53 @@ packages: '@types/yargs@17.0.35': resolution: {integrity: sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg==} - '@typescript/typescript-aix-ppc64@7.0.2': - resolution: {integrity: sha512-MTKKkWB7p/0E9xi1d1tHtZ5PiLkGEMIq88pK2CubZjOsLtYTLqhgIgi6zepFa+9GHZ6h05NMCkQxGKiPXMxXtQ==} - engines: {node: '>=16.20.0'} - cpu: [ppc64] - os: [aix] - - '@typescript/typescript-darwin-arm64@7.0.2': - resolution: {integrity: sha512-gowzar9MwS/aRWp6f3a4KUqzRjAZjOsmGNCM6LcTgXum+dBfgsBVMN+AgvOCCbguXyick6LJhpBszxMebJ8syA==} + '@typescript/native-preview-darwin-arm64@7.0.0-dev.20260512.1': + resolution: {integrity: sha512-l9AJi/TIVMPx5R1c7fxZCSA7eUaHeA0C9Mxdxx/oQJo1K/GtbI3mzYe/SiKNltko1KSdKUmWVhPwxTOS289REg==} engines: {node: '>=16.20.0'} cpu: [arm64] os: [darwin] - '@typescript/typescript-darwin-x64@7.0.2': - resolution: {integrity: sha512-SZ9xZInqApNlNGc9s0W1VSsktYSOe9cFqNOIqmN1Gs8SmkjKZYFt017G4VwPxASInODuAdbTW7sXiFUf893RgA==} + '@typescript/native-preview-darwin-x64@7.0.0-dev.20260512.1': + resolution: {integrity: sha512-oABZLQrfB8JN2Ct2CiLK5PyE28Em3sIJlZsAMD45/A2ymtIaa5826dwv8vapE5Wjp54ao0LXxCSuKFm1A8zzCQ==} engines: {node: '>=16.20.0'} cpu: [x64] os: [darwin] - '@typescript/typescript-freebsd-arm64@7.0.2': - resolution: {integrity: sha512-W5NH4y/J0plIIS5b2xvTEkU7JFxyqdMAOgf+Ilhl0vHQXKO5dZoxd+C/jEtq56c4F3wk71RB4BMRQ2XdI+bwYQ==} - engines: {node: '>=16.20.0'} - cpu: [arm64] - os: [freebsd] - - '@typescript/typescript-freebsd-x64@7.0.2': - resolution: {integrity: sha512-UMGDx5sTpzNw3WiPebH7l90IWfJggEd+egHt/q6p7/Cm3zqoV7VxkGXt+3DxPIw8CcmvAB0j3sVVfbhX+M4Tpw==} - engines: {node: '>=16.20.0'} - cpu: [x64] - os: [freebsd] - - '@typescript/typescript-linux-arm64@7.0.2': - resolution: {integrity: sha512-Qh4eU4/y3yDjnfjjyPYihMj5/ODIlmt+Bzu17OI+fiSRDW57QmU5SiN63exPRNJPKUzcc1INa1NXdrJ+MqHjUQ==} + '@typescript/native-preview-linux-arm64@7.0.0-dev.20260512.1': + resolution: {integrity: sha512-xvbwzpTe+5N6bnBI/t9n4zsGzXxz3V6rVbvDUoJmRLfav5fz+ck0QDkGQGUPrQEEIp0KEzQvx7c+AEZnzdvTQA==} engines: {node: '>=16.20.0'} cpu: [arm64] os: [linux] - '@typescript/typescript-linux-arm@7.0.2': - resolution: {integrity: sha512-gffT3xPz9sR7j/YJExkyPntrI0P2EP9XbOyWzth2/Gs0RstK+90RBcO0ncXoXy/beYll1SXw846Nf2zdnEz0QQ==} + '@typescript/native-preview-linux-arm@7.0.0-dev.20260512.1': + resolution: {integrity: sha512-0Hs1Gqa/t9cthoPdqHud1pFGUr9DgJivBTjwquTUh8jt/6PI2bQxoMNZLiN/bhqeDFDTzdxoMBfCaytsTMcXqw==} engines: {node: '>=16.20.0'} cpu: [arm] os: [linux] - '@typescript/typescript-linux-loong64@7.0.2': - resolution: {integrity: sha512-uEHck9i8hoAzXPiYRib1O7miOnz23SxIeVl6F4LXox+qov1K35jHcEW6VHKvZI+pyvl7fZEP4MCU5LYvIq1GuQ==} - engines: {node: '>=16.20.0'} - cpu: [loong64] - os: [linux] - - '@typescript/typescript-linux-mips64el@7.0.2': - resolution: {integrity: sha512-R4KvAMnE43W5Qeqb0Ly56O3mWMWIAgsMyz36DCaycd5nbg/9kzm0liw3JocfRqyJY0KPmzFjbswozXyW0DnIYA==} - engines: {node: '>=16.20.0'} - cpu: [mips64el] - os: [linux] - - '@typescript/typescript-linux-ppc64@7.0.2': - resolution: {integrity: sha512-DORx5b3sd/4S7eayxm4FQv+A7CrkUIGRaHiwI8oiHTAI1fAPWhF4J0vAlkC8biAlHSVVwxMQ3tjZ2/DVbnQiiA==} - engines: {node: '>=16.20.0'} - cpu: [ppc64] - os: [linux] - - '@typescript/typescript-linux-riscv64@7.0.2': - resolution: {integrity: sha512-wf0jqEDOjrPRnKwYRyyJDRo11KMbvMFrU+q4zqKyChODBzvlkbhNQfKvLxQCcwTpdDaXSHZTVuh0JoCrKCUMHQ==} - engines: {node: '>=16.20.0'} - cpu: [riscv64] - os: [linux] - - '@typescript/typescript-linux-s390x@7.0.2': - resolution: {integrity: sha512-IkwJc3L7yhytWd/ewjyxNDfOmswCm9GWMJT/ue/dU4aZNbwZeYAetq42VyLmsmSjvoX7z74X6ZaYCtzAr0EuGw==} - engines: {node: '>=16.20.0'} - cpu: [s390x] - os: [linux] - - '@typescript/typescript-linux-x64@7.0.2': - resolution: {integrity: sha512-EYdf2cNg7rgCWJnxCdJ+F3V39O8ihb37eHAu1LK8oAFizgTQbPOK7zHHXbPt8rX24COqODXeI3sIf0fCXG7H/A==} + '@typescript/native-preview-linux-x64@7.0.0-dev.20260512.1': + resolution: {integrity: sha512-qr5h6FPo74bN/U+EwRuayBhUbxaji8xzFbIbhMOA2oYSc/qozp5ia2g1+9xGw67MXxPPw/IPT+UGvrNK7K1NeQ==} engines: {node: '>=16.20.0'} cpu: [x64] os: [linux] - '@typescript/typescript-netbsd-arm64@7.0.2': - resolution: {integrity: sha512-+polYF4MF04aPpO5FTkHran9yUQDSXqy5GiSDKpsll5jy3l3+g9QLhpf39T+ePtefhXLOGrLl0QIjkQP6VnelA==} - engines: {node: '>=16.20.0'} - cpu: [arm64] - os: [netbsd] - - '@typescript/typescript-netbsd-x64@7.0.2': - resolution: {integrity: sha512-8YIT0EHM/3dq10ZOVF/A7pc/YSMtbcecct4rWtexrnSCHOPcpC2KTLXfTCR6vDpnSiY12heNb1GiN/wu+T/FyA==} - engines: {node: '>=16.20.0'} - cpu: [x64] - os: [netbsd] - - '@typescript/typescript-openbsd-arm64@7.0.2': - resolution: {integrity: sha512-APT8+ClYnuYm1u9+kgGXoMj2VzWzcymwh2gNSQVySHfkRDGOTVkoWLjCmOQSaO+PoqQ57B0flRp9SA+7GnnkzQ==} - engines: {node: '>=16.20.0'} - cpu: [arm64] - os: [openbsd] - - '@typescript/typescript-openbsd-x64@7.0.2': - resolution: {integrity: sha512-yX7s+Q0Dln0Dt9tEzZsAjXXR/+ytBM7AlglaqyeMPxQszJ1JhlJdZ6jLA+IzldHtflX81em7lDao1xXu+aRRkg==} - engines: {node: '>=16.20.0'} - cpu: [x64] - os: [openbsd] - - '@typescript/typescript-sunos-x64@7.0.2': - resolution: {integrity: sha512-dLJDGaLZ1D4HPQn62u1n8mBDkJREwMsAkCdkwd4Ieqw+x3TUyTsqY0YiBCtE6H6OzzgGk3iuZ3vFWRS+E8/d1g==} - engines: {node: '>=16.20.0'} - cpu: [x64] - os: [sunos] - - '@typescript/typescript-win32-arm64@7.0.2': - resolution: {integrity: sha512-Gyl1Vy6OsWesLzmq+EP0Fb7b4Nid5232AvcA2SFcdYreldpNtYFFofPjnt62y9hQy7VTaZp65ICJjuAQRaVcIQ==} + '@typescript/native-preview-win32-arm64@7.0.0-dev.20260512.1': + resolution: {integrity: sha512-meNWxhNEfaqos2U0JXvfxWvy4JWrKE9fZepCndDZi+t04X+AIiLYp5s6crWnKP67nzVAzMNgTAc8mu8CnGM+/A==} engines: {node: '>=16.20.0'} cpu: [arm64] os: [win32] - '@typescript/typescript-win32-x64@7.0.2': - resolution: {integrity: sha512-0BQ3HkAHHlKLSp1qRvf3SUhGpGsDuhB/jgFw75guyqbxJqEaS0Cw/VFO8i2nHglJUzQCRtMMR/IBAKE3ETMC4g==} + '@typescript/native-preview-win32-x64@7.0.0-dev.20260512.1': + resolution: {integrity: sha512-Hp6vBnxJSKEEAVWgIoWMmfqkZXCdkhm6XTivrwgRzBwWfiTVe2ZyZ7byWegIKeNnBbffg/K2KvoM8JAHl059GQ==} engines: {node: '>=16.20.0'} cpu: [x64] os: [win32] + '@typescript/native-preview@7.0.0-dev.20260512.1': + resolution: {integrity: sha512-KIzYPGuxZnyiiYkYrozDT94Af2nwbdLXoY1cgGY66RRa9HSEw13RH9WHg8wA8fZhT4wYzF5uF7WY3hz0QhaxGg==} + engines: {node: '>=16.20.0'} + hasBin: true + '@ungap/structured-clone@1.3.1': resolution: {integrity: sha512-mUFwbeTqrVgDQxFveS+df2yfap6iuP20NAKAsBt5jDEoOTDew+zwLAOilHCeQJOVSvmgCX4ogqIrA0mnyr08yQ==} @@ -8716,9 +8646,9 @@ packages: resolution: {integrity: sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==} engines: {node: '>= 0.4'} - typescript@7.0.2: - resolution: {integrity: sha512-8FYau96o3NKOhbjKi/qNvG/W5jhzxkbdm5sj9AbZ/5T5sWqn3hJgLfGx27sRKZWTvyzCP8dLRBTf5tBTSRVUNA==} - engines: {node: '>=16.20.0'} + typescript@6.0.3: + resolution: {integrity: sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==} + engines: {node: '>=14.17'} hasBin: true ua-parser-js@0.7.41: @@ -10972,19 +10902,19 @@ snapshots: '@lingui/babel-plugin-extract-messages@5.9.5': {} - '@lingui/babel-plugin-lingui-macro@5.9.5(typescript@7.0.2)': + '@lingui/babel-plugin-lingui-macro@5.9.5(typescript@6.0.3)': dependencies: '@babel/core': 7.29.0 '@babel/runtime': 7.29.2 '@babel/types': 7.29.0 - '@lingui/conf': 5.9.5(typescript@7.0.2) - '@lingui/core': 5.9.5(@lingui/babel-plugin-lingui-macro@5.9.5(typescript@7.0.2)) + '@lingui/conf': 5.9.5(typescript@6.0.3) + '@lingui/core': 5.9.5(@lingui/babel-plugin-lingui-macro@5.9.5(typescript@6.0.3)) '@lingui/message-utils': 5.9.5 transitivePeerDependencies: - supports-color - typescript - '@lingui/cli@5.9.5(typescript@7.0.2)': + '@lingui/cli@5.9.5(typescript@6.0.3)': dependencies: '@babel/core': 7.29.0 '@babel/generator': 7.29.1 @@ -10992,10 +10922,10 @@ snapshots: '@babel/runtime': 7.29.2 '@babel/types': 7.29.0 '@lingui/babel-plugin-extract-messages': 5.9.5 - '@lingui/babel-plugin-lingui-macro': 5.9.5(typescript@7.0.2) - '@lingui/conf': 5.9.5(typescript@7.0.2) - '@lingui/core': 5.9.5(@lingui/babel-plugin-lingui-macro@5.9.5(typescript@7.0.2)) - '@lingui/format-po': 5.9.5(typescript@7.0.2) + '@lingui/babel-plugin-lingui-macro': 5.9.5(typescript@6.0.3) + '@lingui/conf': 5.9.5(typescript@6.0.3) + '@lingui/core': 5.9.5(@lingui/babel-plugin-lingui-macro@5.9.5(typescript@6.0.3)) + '@lingui/format-po': 5.9.5(typescript@6.0.3) '@lingui/message-utils': 5.9.5 chokidar: 3.5.1 cli-table: 0.3.11 @@ -11018,26 +10948,26 @@ snapshots: - supports-color - typescript - '@lingui/conf@5.9.5(typescript@7.0.2)': + '@lingui/conf@5.9.5(typescript@6.0.3)': dependencies: '@babel/runtime': 7.29.2 - cosmiconfig: 8.3.6(typescript@7.0.2) + cosmiconfig: 8.3.6(typescript@6.0.3) jest-validate: 29.7.0 jiti: 2.7.0 picocolors: 1.1.1 transitivePeerDependencies: - typescript - '@lingui/core@5.9.5(@lingui/babel-plugin-lingui-macro@5.9.5(typescript@7.0.2))': + '@lingui/core@5.9.5(@lingui/babel-plugin-lingui-macro@5.9.5(typescript@6.0.3))': dependencies: '@babel/runtime': 7.29.2 '@lingui/message-utils': 5.9.5 optionalDependencies: - '@lingui/babel-plugin-lingui-macro': 5.9.5(typescript@7.0.2) + '@lingui/babel-plugin-lingui-macro': 5.9.5(typescript@6.0.3) - '@lingui/format-po@5.9.5(typescript@7.0.2)': + '@lingui/format-po@5.9.5(typescript@6.0.3)': dependencies: - '@lingui/conf': 5.9.5(typescript@7.0.2) + '@lingui/conf': 5.9.5(typescript@6.0.3) '@lingui/message-utils': 5.9.5 date-fns: 3.6.0 pofile: 1.1.4 @@ -11049,13 +10979,13 @@ snapshots: '@messageformat/parser': 5.1.1 js-sha256: 0.10.1 - '@lingui/react@5.9.5(@lingui/babel-plugin-lingui-macro@5.9.5(typescript@7.0.2))(react@19.1.0)': + '@lingui/react@5.9.5(@lingui/babel-plugin-lingui-macro@5.9.5(typescript@6.0.3))(react@19.1.0)': dependencies: '@babel/runtime': 7.29.2 - '@lingui/core': 5.9.5(@lingui/babel-plugin-lingui-macro@5.9.5(typescript@7.0.2)) + '@lingui/core': 5.9.5(@lingui/babel-plugin-lingui-macro@5.9.5(typescript@6.0.3)) react: 19.1.0 optionalDependencies: - '@lingui/babel-plugin-lingui-macro': 5.9.5(typescript@7.0.2) + '@lingui/babel-plugin-lingui-macro': 5.9.5(typescript@6.0.3) '@messageformat/parser@5.1.1': dependencies: @@ -12633,65 +12563,36 @@ snapshots: dependencies: '@types/yargs-parser': 21.0.3 - '@typescript/typescript-aix-ppc64@7.0.2': + '@typescript/native-preview-darwin-arm64@7.0.0-dev.20260512.1': optional: true - '@typescript/typescript-darwin-arm64@7.0.2': + '@typescript/native-preview-darwin-x64@7.0.0-dev.20260512.1': optional: true - '@typescript/typescript-darwin-x64@7.0.2': + '@typescript/native-preview-linux-arm64@7.0.0-dev.20260512.1': optional: true - '@typescript/typescript-freebsd-arm64@7.0.2': + '@typescript/native-preview-linux-arm@7.0.0-dev.20260512.1': optional: true - '@typescript/typescript-freebsd-x64@7.0.2': + '@typescript/native-preview-linux-x64@7.0.0-dev.20260512.1': optional: true - '@typescript/typescript-linux-arm64@7.0.2': + '@typescript/native-preview-win32-arm64@7.0.0-dev.20260512.1': optional: true - '@typescript/typescript-linux-arm@7.0.2': + '@typescript/native-preview-win32-x64@7.0.0-dev.20260512.1': optional: true - '@typescript/typescript-linux-loong64@7.0.2': - optional: true - - '@typescript/typescript-linux-mips64el@7.0.2': - optional: true - - '@typescript/typescript-linux-ppc64@7.0.2': - optional: true - - '@typescript/typescript-linux-riscv64@7.0.2': - optional: true - - '@typescript/typescript-linux-s390x@7.0.2': - optional: true - - '@typescript/typescript-linux-x64@7.0.2': - optional: true - - '@typescript/typescript-netbsd-arm64@7.0.2': - optional: true - - '@typescript/typescript-netbsd-x64@7.0.2': - optional: true - - '@typescript/typescript-openbsd-arm64@7.0.2': - optional: true - - '@typescript/typescript-openbsd-x64@7.0.2': - optional: true - - '@typescript/typescript-sunos-x64@7.0.2': - optional: true - - '@typescript/typescript-win32-arm64@7.0.2': - optional: true - - '@typescript/typescript-win32-x64@7.0.2': - optional: true + '@typescript/native-preview@7.0.0-dev.20260512.1': + optionalDependencies: + '@typescript/native-preview-darwin-arm64': 7.0.0-dev.20260512.1 + '@typescript/native-preview-darwin-x64': 7.0.0-dev.20260512.1 + '@typescript/native-preview-linux-arm': 7.0.0-dev.20260512.1 + '@typescript/native-preview-linux-arm64': 7.0.0-dev.20260512.1 + '@typescript/native-preview-linux-x64': 7.0.0-dev.20260512.1 + '@typescript/native-preview-win32-arm64': 7.0.0-dev.20260512.1 + '@typescript/native-preview-win32-x64': 7.0.0-dev.20260512.1 '@ungap/structured-clone@1.3.1': {} @@ -13531,14 +13432,14 @@ snapshots: core-util-is@1.0.3: {} - cosmiconfig@8.3.6(typescript@7.0.2): + cosmiconfig@8.3.6(typescript@6.0.3): dependencies: import-fresh: 3.3.1 js-yaml: 4.1.1 parse-json: 5.2.0 path-type: 4.0.0 optionalDependencies: - typescript: 7.0.2 + typescript: 6.0.3 create-jest@29.7.0(@types/node@24.12.4): dependencies: @@ -18205,28 +18106,7 @@ snapshots: possible-typed-array-names: 1.1.0 reflect.getprototypeof: 1.0.10 - typescript@7.0.2: - optionalDependencies: - '@typescript/typescript-aix-ppc64': 7.0.2 - '@typescript/typescript-darwin-arm64': 7.0.2 - '@typescript/typescript-darwin-x64': 7.0.2 - '@typescript/typescript-freebsd-arm64': 7.0.2 - '@typescript/typescript-freebsd-x64': 7.0.2 - '@typescript/typescript-linux-arm': 7.0.2 - '@typescript/typescript-linux-arm64': 7.0.2 - '@typescript/typescript-linux-loong64': 7.0.2 - '@typescript/typescript-linux-mips64el': 7.0.2 - '@typescript/typescript-linux-ppc64': 7.0.2 - '@typescript/typescript-linux-riscv64': 7.0.2 - '@typescript/typescript-linux-s390x': 7.0.2 - '@typescript/typescript-linux-x64': 7.0.2 - '@typescript/typescript-netbsd-arm64': 7.0.2 - '@typescript/typescript-netbsd-x64': 7.0.2 - '@typescript/typescript-openbsd-arm64': 7.0.2 - '@typescript/typescript-openbsd-x64': 7.0.2 - '@typescript/typescript-sunos-x64': 7.0.2 - '@typescript/typescript-win32-arm64': 7.0.2 - '@typescript/typescript-win32-x64': 7.0.2 + typescript@6.0.3: {} ua-parser-js@0.7.41: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index f916bbe6a3..c917be629a 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -43,25 +43,3 @@ patchedDependencies: minimumReleaseAgeExclude: - '@atproto/*' - '@bsky.app/*' - # TODO: remove once these are old enough - - 'typescript' - - '@typescript/typescript-aix-ppc64@7.0.2' - - '@typescript/typescript-darwin-arm64@7.0.2' - - '@typescript/typescript-darwin-x64@7.0.2' - - '@typescript/typescript-freebsd-arm64@7.0.2' - - '@typescript/typescript-freebsd-x64@7.0.2' - - '@typescript/typescript-linux-arm64@7.0.2' - - '@typescript/typescript-linux-arm@7.0.2' - - '@typescript/typescript-linux-loong64@7.0.2' - - '@typescript/typescript-linux-mips64el@7.0.2' - - '@typescript/typescript-linux-ppc64@7.0.2' - - '@typescript/typescript-linux-riscv64@7.0.2' - - '@typescript/typescript-linux-s390x@7.0.2' - - '@typescript/typescript-linux-x64@7.0.2' - - '@typescript/typescript-netbsd-arm64@7.0.2' - - '@typescript/typescript-netbsd-x64@7.0.2' - - '@typescript/typescript-openbsd-arm64@7.0.2' - - '@typescript/typescript-openbsd-x64@7.0.2' - - '@typescript/typescript-sunos-x64@7.0.2' - - '@typescript/typescript-win32-arm64@7.0.2' - - '@typescript/typescript-win32-x64@7.0.2' From 4c111eb45eab45fb88a8e301db24c70bfe1f513b Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Tue, 14 Jul 2026 20:15:10 +0300 Subject: [PATCH 27/52] Stub out react-native-svg fetchData on web to drop buffer polyfill (#11097) Co-authored-by: Claude Fable 5 --- webpack.config.js | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/webpack.config.js b/webpack.config.js index bccf95adef..889a2cb83f 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -1,3 +1,5 @@ +const path = require('path') + const createExpoWebpackConfigAsync = require('@expo/webpack-config') const {withAlias} = require('@expo/webpack-config/addons') const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin') @@ -46,11 +48,34 @@ module.exports = async function (env, argv) { dangerouslyAddModulePathsToTranspile: ['@bsky.app/expo', '@atproto/api'], } let config = await createExpoWebpackConfigAsync(env, argv) + /* + * Expo only registers its own internal config as a cache build dependency, + * so changes to this file (e.g. aliases) don't invalidate the persistent + * filesystem cache and stale module resolutions get reused. Register this + * file so edits here always bust the cache. + */ + if (config.cache?.buildDependencies) { + config.cache.buildDependencies.config = [ + ...(config.cache.buildDependencies.config || []), + __filename, + ] + } config = withAlias(config, { 'react-native$': 'react-native-web', 'react-native-webview': 'react-native-web-webview', 'react-native-gesture-handler': false, // RNGH should not be used on web, so let's cause a build error if it sneaks in '@sentry-internal/replay': false, // not used, ~300kb of dead weight + /* + * react-native-svg's fetchData util imports the ~55KB `buffer` polyfill, + * but is only needed by SvgUri/SvgXml remote loading, which we don't use. + * Stubbing it out makes fetchText undefined, so it throws if ever called. + * The alias key must be an absolute path because the package imports it + * via a relative path internally. + */ + [path.join( + __dirname, + 'node_modules/react-native-svg/lib/module/utils/fetchData', + )]: false, }) // react-native-uuid ships sourceMappingURL comments but no .map files. From f42a325f49a48958c6f11a8f7b2922de50517496 Mon Sep 17 00:00:00 2001 From: Spence Pope Date: Tue, 14 Jul 2026 13:15:28 -0400 Subject: [PATCH 28/52] fix web feed footer clearing the bottom bar (#11151) --- src/view/com/posts/PostFeed.tsx | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/src/view/com/posts/PostFeed.tsx b/src/view/com/posts/PostFeed.tsx index e502ca028c..42274790c7 100644 --- a/src/view/com/posts/PostFeed.tsx +++ b/src/view/com/posts/PostFeed.tsx @@ -22,6 +22,7 @@ import {useLingui} from '@lingui/react/macro' import {useQueryClient} from '@tanstack/react-query' import {DISCOVER_FEED_URI, KNOWN_SHUTDOWN_FEEDS} from '#/lib/constants' +import {useBottomBarOffset} from '#/lib/hooks/useBottomBarOffset' import {useInitialNumToRender} from '#/lib/hooks/useInitialNumToRender' import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback' import {isNetworkError} from '#/lib/strings/errors' @@ -885,13 +886,17 @@ let PostFeed = ({ const shouldRenderEndOfFeed = !hasNextPage && !isEmpty && !isFetching && !isError && !!renderEndOfFeed + const bottomBarOffset = useBottomBarOffset() const FeedFooter = useCallback(() => { - /** + /* * A bit of padding at the bottom of the feed as you scroll and when you * reach the end, so that content isn't cut off by the bottom of the - * screen. + * screen. On mobile web, also clear the fixed bottom bar; on native the + * doubled headerOffset already covers the tab bar. */ - const offset = Math.max(headerOffset, 32) * (IS_WEB ? 1 : 2) + const offset = + Math.max(headerOffset, 32) * (IS_WEB ? 1 : 2) + + (IS_WEB ? bottomBarOffset : 0) return isFetchingNextPage ? ( @@ -903,7 +908,13 @@ let PostFeed = ({ ) : ( ) - }, [isFetchingNextPage, shouldRenderEndOfFeed, renderEndOfFeed, headerOffset]) + }, [ + isFetchingNextPage, + shouldRenderEndOfFeed, + renderEndOfFeed, + headerOffset, + bottomBarOffset, + ]) const liveNowConfig = useLiveNowConfig() From 581aee5472b481955901ef35efcef4a3e590dadc Mon Sep 17 00:00:00 2001 From: pfrazee <1270099+pfrazee@users.noreply.github.com> Date: Wed, 15 Jul 2026 03:10:36 +0000 Subject: [PATCH 29/52] Nightly source-language update --- src/locale/locales/en/messages.po | 90 ++++++++++++++++--------------- 1 file changed, 46 insertions(+), 44 deletions(-) diff --git a/src/locale/locales/en/messages.po b/src/locale/locales/en/messages.po index 9f59df11c3..f2320b8ae7 100644 --- a/src/locale/locales/en/messages.po +++ b/src/locale/locales/en/messages.po @@ -1631,7 +1631,7 @@ msgstr "" msgid "An error occurred while hiding suggestion. {0}" msgstr "" -#: src/components/Post/Embed/VideoEmbed/index.tsx:149 +#: src/components/Post/Embed/VideoEmbed/index.tsx:188 msgid "An error occurred while loading the video. Please try again later." msgstr "" @@ -1765,7 +1765,7 @@ msgstr "Any date" msgid "Anyone" msgstr "" -#: src/view/com/composer/threadgate/ThreadgateBtn.tsx:137 +#: src/view/com/composer/threadgate/ThreadgateBtn.tsx:135 msgid "Anyone can interact" msgstr "" @@ -2120,7 +2120,7 @@ msgstr "" msgid "Before you can accept this chat request, you must first verify your email." msgstr "" -#: src/components/activity-notifications/SubscribeProfileButton.tsx:59 +#: src/components/activity-notifications/SubscribeProfileButton.tsx:56 msgid "Before you can get notifications for {name}'s posts, you must first verify your email." msgstr "" @@ -4188,7 +4188,7 @@ msgstr "" msgid "Double tap to close the dialog" msgstr "" -#: src/screens/VideoFeed/index.tsx:1107 +#: src/screens/VideoFeed/index.tsx:1161 msgid "Double tap to like" msgstr "" @@ -4605,7 +4605,7 @@ msgstr "" msgid "Enter your username and password" msgstr "" -#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerNative.tsx:156 +#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerNative.tsx:163 msgid "Enters full screen" msgstr "" @@ -4718,7 +4718,7 @@ msgstr "" msgid "Expand post text" msgstr "" -#: src/screens/VideoFeed/index.tsx:983 +#: src/screens/VideoFeed/index.tsx:1037 msgid "Expands or collapses post text" msgstr "" @@ -5387,7 +5387,7 @@ msgstr "Focus the search field" #: src/screens/Messages/ConversationSettings/Member.tsx:170 #: src/screens/PostThread/components/ThreadItemAnchorFollowButton.tsx:157 #: src/screens/Profile/Header/ProfileHeaderStandard.tsx:409 -#: src/screens/VideoFeed/index.tsx:866 +#: src/screens/VideoFeed/index.tsx:920 #: src/view/com/notifications/NotificationFeedItem.tsx:858 #: src/view/com/notifications/NotificationFeedItem.tsx:865 msgid "Follow" @@ -5403,7 +5403,7 @@ msgstr "" msgid "Follow {displayName}" msgstr "Follow {displayName}" -#: src/screens/VideoFeed/index.tsx:845 +#: src/screens/VideoFeed/index.tsx:899 msgid "Follow {handle}" msgstr "" @@ -5502,7 +5502,7 @@ msgstr "" #: src/components/ProfileHoverCard/index.web.tsx:505 #: src/screens/PostThread/components/ThreadItemAnchorFollowButton.tsx:160 #: src/screens/Profile/Header/ProfileHeaderStandard.tsx:405 -#: src/screens/VideoFeed/index.tsx:864 +#: src/screens/VideoFeed/index.tsx:918 #: src/view/com/notifications/NotificationFeedItem.tsx:836 #: src/view/com/notifications/NotificationFeedItem.tsx:853 msgid "Following" @@ -5527,7 +5527,7 @@ msgstr "" msgid "Following {displayName}" msgstr "Following {displayName}" -#: src/screens/VideoFeed/index.tsx:844 +#: src/screens/VideoFeed/index.tsx:898 msgid "Following {handle}" msgstr "" @@ -5720,7 +5720,8 @@ msgstr "Get notifications when people send you messages." msgid "Get notifications when there's activity on posts you're subscribed to." msgstr "Get notifications when there's activity on posts you're subscribed to." -#: src/components/activity-notifications/SubscribeProfileButton.tsx:94 +#: src/components/activity-notifications/SubscribeProfileButton.tsx:89 +#: src/components/activity-notifications/SubscribeProfileButton.tsx:90 msgid "Get notified about new posts" msgstr "" @@ -5732,7 +5733,7 @@ msgstr "" msgid "Get notified of this account’s activity" msgstr "" -#: src/components/activity-notifications/SubscribeProfileButton.tsx:87 +#: src/components/activity-notifications/SubscribeProfileButton.tsx:84 msgid "Get notified when {name} posts" msgstr "" @@ -5775,8 +5776,8 @@ msgstr "" #: src/screens/ProfileList/components/ErrorScreen.tsx:35 #: src/screens/ProfileList/components/ErrorScreen.tsx:41 #: src/screens/VideoFeed/components/Header.tsx:163 -#: src/screens/VideoFeed/index.tsx:1168 -#: src/screens/VideoFeed/index.tsx:1172 +#: src/screens/VideoFeed/index.tsx:1222 +#: src/screens/VideoFeed/index.tsx:1226 #: src/view/com/auth/LoggedOut.tsx:127 #: src/view/com/profile/ProfileFollowers.tsx:211 #: src/view/com/profile/ProfileFollowers.tsx:212 @@ -6093,7 +6094,7 @@ msgstr "" msgid "Hidden" msgstr "" -#: src/screens/VideoFeed/index.tsx:643 +#: src/screens/VideoFeed/index.tsx:697 msgid "Hidden by your moderation settings." msgstr "" @@ -6560,7 +6561,7 @@ msgstr "" msgid "Input the code which has been emailed to you" msgstr "" -#: src/view/com/composer/threadgate/ThreadgateBtn.tsx:138 +#: src/view/com/composer/threadgate/ThreadgateBtn.tsx:136 msgid "Interaction limited" msgstr "" @@ -7645,7 +7646,7 @@ msgstr "" msgid "Mute" msgstr "Mute" -#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerNative.tsx:177 +#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerNative.tsx:184 #: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/VolumeControl.tsx:97 msgctxt "video" msgid "Mute" @@ -8629,7 +8630,7 @@ msgstr "" msgid "Opens a dialog to change the hosting provider you sign in to" msgstr "Opens a dialog to change the hosting provider you sign in to" -#: src/view/com/composer/threadgate/ThreadgateBtn.tsx:154 +#: src/view/com/composer/threadgate/ThreadgateBtn.tsx:151 msgid "Opens a dialog to choose who can interact with this post" msgstr "" @@ -8862,7 +8863,7 @@ msgstr "" msgid "Password updated!" msgstr "" -#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerNative.tsx:161 +#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerNative.tsx:168 #: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/VideoControls.tsx:395 msgid "Pause" msgstr "" @@ -9008,7 +9009,7 @@ msgstr "" msgid "Pinned to your feeds" msgstr "" -#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerNative.tsx:161 +#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerNative.tsx:168 #: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/VideoControls.tsx:396 msgid "Play" msgstr "" @@ -9022,7 +9023,7 @@ msgstr "" msgid "Play GIF" msgstr "" -#: src/components/Post/Embed/VideoEmbed/index.tsx:130 +#: src/components/Post/Embed/VideoEmbed/index.tsx:169 #: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/VideoControls.tsx:347 msgid "Play video" msgstr "" @@ -9035,7 +9036,7 @@ msgstr "" msgid "Plays or pauses the GIF" msgstr "" -#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerNative.tsx:162 +#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerNative.tsx:169 msgid "Plays or pauses the video" msgstr "" @@ -9268,7 +9269,7 @@ msgstr "" #: src/screens/PostThread/components/ThreadItemAnchor.tsx:132 #: src/screens/PostThread/components/ThreadItemPost.tsx:116 #: src/screens/PostThread/components/ThreadItemTreePost.tsx:109 -#: src/screens/VideoFeed/index.tsx:547 +#: src/screens/VideoFeed/index.tsx:551 msgid "Post has been deleted" msgstr "" @@ -9462,7 +9463,8 @@ msgstr "" msgid "Promoting or selling prohibited items or services" msgstr "" -#: src/view/com/composer/threadgate/ThreadgateBtn.tsx:165 +#: src/view/com/composer/threadgate/ThreadgateBtn.tsx:160 +#: src/view/com/composer/threadgate/ThreadgateBtn.tsx:161 msgid "Psst! You can edit who can interact with this post." msgstr "" @@ -9602,11 +9604,11 @@ msgstr "Read about how to use advanced search filters" msgid "Read blog post" msgstr "" -#: src/screens/VideoFeed/index.tsx:984 +#: src/screens/VideoFeed/index.tsx:1038 msgid "Read less" msgstr "" -#: src/screens/VideoFeed/index.tsx:984 +#: src/screens/VideoFeed/index.tsx:1038 msgid "Read more" msgstr "" @@ -10340,7 +10342,7 @@ msgstr "" #: src/screens/ProfileList/components/ErrorScreen.tsx:36 #: src/screens/Settings/components/ChangeHandleDialog.tsx:577 -#: src/screens/VideoFeed/index.tsx:1169 +#: src/screens/VideoFeed/index.tsx:1223 #: src/view/screens/NotFound.tsx:54 msgid "Returns to previous page" msgstr "" @@ -11168,8 +11170,8 @@ msgstr "" #: src/features/liveNow/components/LiveStatusDialog.tsx:318 #: src/features/liveNow/components/LiveStatusDialog.tsx:322 #: src/screens/List/ListHiddenScreen.tsx:194 -#: src/screens/VideoFeed/index.tsx:646 -#: src/screens/VideoFeed/index.tsx:652 +#: src/screens/VideoFeed/index.tsx:700 +#: src/screens/VideoFeed/index.tsx:706 msgid "Show anyway" msgstr "" @@ -12030,7 +12032,7 @@ msgstr "" msgid "That's all, folks!" msgstr "" -#: src/screens/VideoFeed/index.tsx:1141 +#: src/screens/VideoFeed/index.tsx:1195 msgid "That's everything!" msgstr "" @@ -12197,7 +12199,7 @@ msgid "There was an issue fetching notifications. Tap here to try again." msgstr "" #: src/screens/Search/Explore.tsx:1027 -#: src/view/com/posts/PostFeed.tsx:777 +#: src/view/com/posts/PostFeed.tsx:778 msgid "There was an issue fetching posts. Tap here to try again." msgstr "" @@ -12742,7 +12744,7 @@ msgstr "" msgid "Toggle to enable or disable adult content" msgstr "" -#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerNative.tsx:179 +#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerNative.tsx:186 msgid "Toggles the sound" msgstr "" @@ -13017,7 +13019,7 @@ msgstr "" msgid "Unfollow account" msgstr "" -#: src/screens/VideoFeed/index.tsx:848 +#: src/screens/VideoFeed/index.tsx:902 msgid "Unfollows the user" msgstr "" @@ -13068,7 +13070,7 @@ msgstr "Unlock this group chat" msgid "Unmute" msgstr "" -#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerNative.tsx:176 +#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerNative.tsx:183 #: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/VolumeControl.tsx:96 msgctxt "video" msgid "Unmute" @@ -13522,8 +13524,8 @@ msgstr "" msgid "Version {0}" msgstr "" -#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerNative.tsx:88 -#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerNative.tsx:155 +#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerNative.tsx:95 +#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerNative.tsx:162 msgid "Video" msgstr "" @@ -13541,7 +13543,7 @@ msgid "Video from {0}: {text}" msgstr "" #. placeholder {0}: sanitizeHandle( post.author.handle, '@', ) -#: src/screens/VideoFeed/index.tsx:1103 +#: src/screens/VideoFeed/index.tsx:1157 msgid "Video from {0}. Tap to play or pause the video" msgstr "" @@ -13550,11 +13552,11 @@ msgstr "" msgid "Video Games" msgstr "" -#: src/screens/VideoFeed/index.tsx:1102 +#: src/screens/VideoFeed/index.tsx:1156 msgid "Video is paused" msgstr "" -#: src/screens/VideoFeed/index.tsx:1102 +#: src/screens/VideoFeed/index.tsx:1156 msgid "Video is playing" msgstr "" @@ -13571,7 +13573,7 @@ msgid "Video uploaded" msgstr "" #. placeholder {0}: embed.alt -#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerNative.tsx:88 +#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerNative.tsx:95 msgid "Video: {0}" msgstr "" @@ -13606,7 +13608,7 @@ msgstr "" #. placeholder {0}: profile.handle #: src/screens/Profile/components/ProfileFeedHeader.tsx:454 #: src/screens/Search/components/SearchProfileCard.tsx:37 -#: src/screens/VideoFeed/index.tsx:809 +#: src/screens/VideoFeed/index.tsx:863 #: src/view/com/notifications/NotificationFeedItem.tsx:613 msgid "View {0}'s profile" msgstr "" @@ -13637,8 +13639,8 @@ msgstr "" msgid "View debug entry" msgstr "" -#: src/screens/VideoFeed/index.tsx:674 -#: src/screens/VideoFeed/index.tsx:692 +#: src/screens/VideoFeed/index.tsx:728 +#: src/screens/VideoFeed/index.tsx:746 msgid "View details" msgstr "" @@ -14719,7 +14721,7 @@ msgstr "" msgid "You've reached your daily limit for video uploads (too many videos)" msgstr "" -#: src/screens/VideoFeed/index.tsx:1150 +#: src/screens/VideoFeed/index.tsx:1204 msgid "You've run out of videos to watch. Maybe it's a good time to take a break?" msgstr "" From 821e1b838a5e959374bcb927dd37f65179141526 Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Wed, 15 Jul 2026 02:53:40 -0500 Subject: [PATCH 30/52] [APP-2642] Social proof text is truncated on mobile (#11155) --- .../PostThread/components/LikesStat.tsx | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/src/screens/PostThread/components/LikesStat.tsx b/src/screens/PostThread/components/LikesStat.tsx index 7b6182d3c0..9a0c7139fb 100644 --- a/src/screens/PostThread/components/LikesStat.tsx +++ b/src/screens/PostThread/components/LikesStat.tsx @@ -5,10 +5,11 @@ import {Plural, Trans, useLingui} from '@lingui/react/macro' import {makeProfileLink} from '#/lib/routes/links' import {sanitizeDisplayName} from '#/lib/strings/display-names' +import {enforceLen} from '#/lib/strings/helpers' import {useModerationOpts} from '#/state/preferences/moderation-opts' import {useLikedBySampleQuery} from '#/state/queries/post-liked-by' import {useSession} from '#/state/session' -import {atoms as a, useTheme} from '#/alf' +import {atoms as a, useBreakpoints, useTheme} from '#/alf' import {AvatarStack} from '#/components/AvatarStack' import {InlineLinkText, Link} from '#/components/Link' import {useFormatPostStatCount} from '#/components/PostControls/util' @@ -17,6 +18,7 @@ import {Text} from '#/components/Typography' import {useAnalytics} from '#/analytics' const AVI_SIZE = 20 +const MAX_NAME_LENGTH = 16 /** * The likes stat for the expanded anchor post. When the viewer follows some @@ -32,6 +34,7 @@ const AVI_SIZE = 20 */ export function LikesStat({post}: {post: AppBskyFeedDefs.PostView}) { const t = useTheme() + const {gtMobile} = useBreakpoints() const {t: l} = useLingui() const {hasSession, currentAccount} = useSession() const moderationOpts = useModerationOpts() @@ -98,15 +101,20 @@ export function LikesStat({post}: {post: AppBskyFeedDefs.PostView}) { const aviStackProfiles = knownLikersAndModeration .slice(0, 3) .map(({actor}) => actor) + const maxNames = gtMobile ? 2 : 1 const names = knownLikersAndModeration - .slice(0, 2) + .slice(0, maxNames) .map(({actor, moderation}) => { return { did: actor.did, href: makeProfileLink(actor), - displayName: sanitizeDisplayName( - actor.displayName || actor.handle, - moderation.ui('displayName'), + displayName: enforceLen( + sanitizeDisplayName( + actor.displayName || actor.handle, + moderation.ui('displayName'), + ), + MAX_NAME_LENGTH, + true, ), } }) From d7f40b7e7fdbc076ef7f6fbc63bd2f87525ca422 Mon Sep 17 00:00:00 2001 From: Spence Pope Date: Wed, 15 Jul 2026 07:36:04 -0400 Subject: [PATCH 31/52] fix android carousel jiggle from cdn-rounded aspect ratio (#11153) --- src/components/images/Gallery/index.tsx | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/src/components/images/Gallery/index.tsx b/src/components/images/Gallery/index.tsx index 12067c2250..c87957503f 100644 --- a/src/components/images/Gallery/index.tsx +++ b/src/components/images/Gallery/index.tsx @@ -485,9 +485,17 @@ function GalleryImage({ loading={index === 0 ? 'eager' : 'lazy'} style={[dims]} onLoad={e => { - const ar = getAspectRatio(e.source) - if (ar && ar !== aspectRatio) { - setAspectRatio(ar) + /* + * Only sync from the loaded thumb if we don't already have a + * ratio from the record. The CDN scales thumbs to integer pixel + * dims, so its reported ratio drifts a fraction from the + * record's - re-syncing would jiggle the FlatList item widths. + */ + if (aspectRatio === undefined) { + const ar = getAspectRatio(e.source) + if (ar !== undefined) { + setAspectRatio(ar) + } } onThumbDims(index, { width: e.source.width, From 3c5c11c0025e5d7ebaaa6ddade3191f522cf8a7f Mon Sep 17 00:00:00 2001 From: DS Boyce <260543580+ds-boyce@users.noreply.github.com> Date: Wed, 15 Jul 2026 06:06:54 -0700 Subject: [PATCH 32/52] Add setting for opting in to beta features (#11123) --- oxlint-suppressions.json | 16 -- package.json | 6 +- pnpm-lock.yaml | 84 +++---- src/App.tsx | 2 + src/App.web.tsx | 2 + src/Navigation.tsx | 9 + src/analytics/features/index.ts | 97 +++++++- src/analytics/features/types.ts | 4 + src/analytics/index.tsx | 55 ++++- src/analytics/metadata.ts | 3 +- src/lib/routes/types.ts | 1 + src/routes.ts | 1 + src/screens/Settings/AccountSettings.tsx | 29 +-- src/screens/Settings/BetaFeaturesSettings.tsx | 233 ++++++++++++++++++ src/screens/Settings/Settings.tsx | 9 + .../components/BetaFeaturesFeedbackDialog.tsx | 159 ++++++++++++ src/state/preferences/beta-user-sync.tsx | 37 +++ src/state/queries/preferences/const.ts | 1 + src/state/queries/preferences/index.ts | 15 ++ src/storage/schema.ts | 12 + 20 files changed, 685 insertions(+), 90 deletions(-) create mode 100644 src/screens/Settings/BetaFeaturesSettings.tsx create mode 100644 src/screens/Settings/components/BetaFeaturesFeedbackDialog.tsx create mode 100644 src/state/preferences/beta-user-sync.tsx diff --git a/oxlint-suppressions.json b/oxlint-suppressions.json index 21dd9f702b..b5eb42488b 100644 --- a/oxlint-suppressions.json +++ b/oxlint-suppressions.json @@ -93,17 +93,6 @@ "count": 2 } }, - "src/analytics/features/index.ts": { - "typescript/no-floating-promises": { - "count": 1 - }, - "typescript/no-misused-promises": { - "count": 1 - }, - "typescript/require-await": { - "count": 1 - } - }, "src/analytics/identifiers/session.test.ts": { "typescript/no-unsafe-call": { "count": 9 @@ -112,11 +101,6 @@ "count": 9 } }, - "src/analytics/metadata.ts": { - "typescript/no-explicit-any": { - "count": 1 - } - }, "src/analytics/metrics/client.test.ts": { "typescript/no-explicit-any": { "count": 1 diff --git a/package.json b/package.json index 3dd3663a32..2542931936 100644 --- a/package.json +++ b/package.json @@ -93,9 +93,9 @@ "prettier": "prettier --check ." }, "dependencies": { - "@atproto/api": "0.20.25", - "@atproto/common-web": "0.5.3", - "@atproto/syntax": "0.6.4", + "@atproto/api": "0.20.28", + "@atproto/common-web": "0.5.6", + "@atproto/syntax": "0.7.2", "@bitdrift/react-native": "^0.6.8", "@braintree/sanitize-url": "^6.0.2", "@bsky.app/alf": "^0.1.14", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 70e5e2ccf1..51468f2236 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -242,14 +242,14 @@ importers: .: dependencies: '@atproto/api': - specifier: 0.20.25 - version: 0.20.25 + specifier: 0.20.28 + version: 0.20.28 '@atproto/common-web': - specifier: 0.5.3 - version: 0.5.3 + specifier: 0.5.6 + version: 0.5.6 '@atproto/syntax': - specifier: 0.6.4 - version: 0.6.4 + specifier: 0.7.2 + version: 0.7.2 '@bitdrift/react-native': specifier: ^0.6.8 version: 0.6.14(react-native@0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) @@ -862,32 +862,32 @@ packages: graphql: optional: true - '@atproto/api@0.20.25': - resolution: {integrity: sha512-PTwt6X0U45C9vikr8NRi0Qzcep9VqJet52HtfcxgG9R7ofRHxqLVPzfVuN/f2xOaYM6H5IG/Nk1E9kXZcI0mSQ==} + '@atproto/api@0.20.28': + resolution: {integrity: sha512-/Rvk8zt9mtRi9tlMD2Qg+NG2lMj3B0HDjmfswR5724pH7GsOEMDDHwleVlmOBSrgACIyVSa5tdCDJ+R+SEhwww==} engines: {node: '>=22'} - '@atproto/common-web@0.5.3': - resolution: {integrity: sha512-FkMhOcNv1y7r5984+zXB+uMN4zJ4QFLEbZrYyQo6bNCf/Kfav/pEHXXENlaZEOweq2NYXf+tr2giMssBuM9u5A==} + '@atproto/common-web@0.5.6': + resolution: {integrity: sha512-5Y4MIK9dpkJPiKiE6u7iEHitxj+g3aAU2GfGL686JlKE2zDKD4y18BJb+uVek6nXQKb5XOdNVvw+7BHarpn8Fw==} engines: {node: '>=22'} - '@atproto/lex-data@0.1.4': - resolution: {integrity: sha512-f9U95sk0zUtxHktvK59peU+Shd0cIUYV68p//GS7sfdkAxUIeaspvX6CY+Quv9Oa4aozmsXI52SjaNn1wuU8RQ==} + '@atproto/lex-data@0.1.5': + resolution: {integrity: sha512-TEM6GHuYpNm4O90LjNgbYq1Gmcr875S+BHrDxkg4PB5w/nlsz4HlbkGQG/WP/xbIV5O8TF5nNHDpCZ5ezTRrFA==} engines: {node: '>=22'} - '@atproto/lex-json@0.1.3': - resolution: {integrity: sha512-Ch2w9bCLFOwWINFFxpZo6DBW7+ZxkehciU3P8N94gSyENLe3/5WWXHdHvkiH7EXZrpI7fKY8nVWn4GIL0ttqxw==} + '@atproto/lex-json@0.1.4': + resolution: {integrity: sha512-ENR2cWkVrES+UL6TovbCRdX9BJOyHHJUS8jYx3Lxp3j4vEphjn/u+DW7bWloST2O8ID2OuVlt6+28ftNEEjmQQ==} engines: {node: '>=22'} - '@atproto/lexicon@0.7.4': - resolution: {integrity: sha512-ulk4RGwMBp4vbkTOZcIwZJeTEPlj3PImOnx9TqxSxMDnBdySxarpd0Aprg7+iAvGyKTsMcrYHpeoLukZaquk0A==} + '@atproto/lexicon@0.7.7': + resolution: {integrity: sha512-92VH2oEsJdrIVNy7WY8rGn99ANNVglyUffoN7GJc0mxKi+fXN5iVlJ2cOyTfYABhr2ldSiptZWXEPEdBaIR9/A==} engines: {node: '>=22'} - '@atproto/syntax@0.6.4': - resolution: {integrity: sha512-ELgpShRGMF65cvLXcoMCZFL0HBzy67yz/Nlhox3yOtTh120B09KjjvZYXhMysVog3nUJqv2EW5rf1VRYCeM/hw==} + '@atproto/syntax@0.7.2': + resolution: {integrity: sha512-tZ1Tr0R9pK4bI4Zs69t29cjMlCFQvRNBeNkqJC5pGeNuCt64D0eoF7s/AlqeVmYppClmQ0xJquggGgsMDP6j7w==} engines: {node: '>=22'} - '@atproto/xrpc@0.8.3': - resolution: {integrity: sha512-0gUGN71+bSqV3PI5U4cQ4fdnw4nI0eD6czHDQ6jB7hMYBwNcJpwAyfA9W+C1G1wayIvP0SIap/M0H/pSKDWFIQ==} + '@atproto/xrpc@0.8.6': + resolution: {integrity: sha512-yVfKrlwZBBm44Ft9jDvHcTCwQ1ElqSlzXHWhT/KcqE+u24EAhWVFosfABfGbUByB/PmN8eLJ06FsWo5pUQcMNQ==} engines: {node: '>=22'} '@babel/code-frame@7.10.4': @@ -8662,9 +8662,6 @@ packages: uc.micro@2.1.0: resolution: {integrity: sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==} - uint8arrays@5.1.1: - resolution: {integrity: sha512-9muQwa4wZG4dKi9gMAIBtnk2Pw87SRpvWTH6lOGm19V2Uqxr4uomUf2PGqPnWc+qs06sN8owUU4jfcoWOcfwVQ==} - unbox-primitive@1.1.0: resolution: {integrity: sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==} engines: {node: '>= 0.4'} @@ -9122,51 +9119,50 @@ snapshots: '@0no-co/graphql.web@1.2.0': {} - '@atproto/api@0.20.25': + '@atproto/api@0.20.28': dependencies: - '@atproto/common-web': 0.5.3 - '@atproto/lexicon': 0.7.4 - '@atproto/syntax': 0.6.4 - '@atproto/xrpc': 0.8.3 + '@atproto/common-web': 0.5.6 + '@atproto/lexicon': 0.7.7 + '@atproto/syntax': 0.7.2 + '@atproto/xrpc': 0.8.6 await-lock: 3.0.0 multiformats: 13.4.2 tlds: 1.261.0 zod: 3.25.76 - '@atproto/common-web@0.5.3': + '@atproto/common-web@0.5.6': dependencies: - '@atproto/lex-data': 0.1.4 - '@atproto/lex-json': 0.1.3 - '@atproto/syntax': 0.6.4 + '@atproto/lex-data': 0.1.5 + '@atproto/lex-json': 0.1.4 + '@atproto/syntax': 0.7.2 zod: 3.25.76 - '@atproto/lex-data@0.1.4': + '@atproto/lex-data@0.1.5': dependencies: multiformats: 13.4.2 tslib: 2.8.1 - uint8arrays: 5.1.1 unicode-segmenter: 0.14.5 - '@atproto/lex-json@0.1.3': + '@atproto/lex-json@0.1.4': dependencies: - '@atproto/lex-data': 0.1.4 + '@atproto/lex-data': 0.1.5 tslib: 2.8.1 - '@atproto/lexicon@0.7.4': + '@atproto/lexicon@0.7.7': dependencies: - '@atproto/common-web': 0.5.3 - '@atproto/syntax': 0.6.4 + '@atproto/common-web': 0.5.6 + '@atproto/syntax': 0.7.2 multiformats: 13.4.2 zod: 3.25.76 - '@atproto/syntax@0.6.4': + '@atproto/syntax@0.7.2': dependencies: iso-datestring-validator: 2.2.2 tslib: 2.8.1 - '@atproto/xrpc@0.8.3': + '@atproto/xrpc@0.8.6': dependencies: - '@atproto/lexicon': 0.7.4 + '@atproto/lexicon': 0.7.7 zod: 3.25.76 '@babel/code-frame@7.10.4': @@ -18114,10 +18110,6 @@ snapshots: uc.micro@2.1.0: {} - uint8arrays@5.1.1: - dependencies: - multiformats: 13.4.2 - unbox-primitive@1.1.0: dependencies: call-bound: 1.0.4 diff --git a/src/App.tsx b/src/App.tsx index 03481ce012..2af2146d92 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -33,6 +33,7 @@ import {Provider as HomeBadgeProvider} from '#/state/home-badge' import {MessagesProvider} from '#/state/messages' import {init as initPersistedState} from '#/state/persisted' import {Provider as PrefsStateProvider} from '#/state/preferences' +import {BetaUserStorageSync} from '#/state/preferences/beta-user-sync' import {Provider as LabelDefsProvider} from '#/state/preferences/label-defs' import {Provider as ModerationOptsProvider} from '#/state/preferences/moderation-opts' import {Provider as UnreadNotifsProvider} from '#/state/queries/notifications/unread' @@ -152,6 +153,7 @@ function InnerApp() { key={currentAccount?.did}> + diff --git a/src/App.web.tsx b/src/App.web.tsx index aa8550bbc2..8070c4d333 100644 --- a/src/App.web.tsx +++ b/src/App.web.tsx @@ -26,6 +26,7 @@ import {Provider as HomeBadgeProvider} from '#/state/home-badge' import {MessagesProvider} from '#/state/messages' import {init as initPersistedState} from '#/state/persisted' import {Provider as PrefsStateProvider} from '#/state/preferences' +import {BetaUserStorageSync} from '#/state/preferences/beta-user-sync' import {Provider as LabelDefsProvider} from '#/state/preferences/label-defs' import {Provider as ModerationOptsProvider} from '#/state/preferences/moderation-opts' import {Provider as UnreadNotifsProvider} from '#/state/queries/notifications/unread' @@ -132,6 +133,7 @@ function InnerApp() { key={currentAccount?.did}> + diff --git a/src/Navigation.tsx b/src/Navigation.tsx index 7bf45b41dc..76fa7187b2 100644 --- a/src/Navigation.tsx +++ b/src/Navigation.tsx @@ -108,6 +108,7 @@ import {AppearanceSettingsScreen} from '#/screens/Settings/AppearanceSettings' import {AppIconSettingsScreen} from '#/screens/Settings/AppIconSettings' import {AppPasswordsScreen} from '#/screens/Settings/AppPasswords' import {AutomationLabelSettingsScreen} from '#/screens/Settings/AutomationLabelSettings' +import {BetaFeaturesSettingsScreen} from '#/screens/Settings/BetaFeaturesSettings' import {ContentAndMediaSettingsScreen} from '#/screens/Settings/ContentAndMediaSettings' import {ExternalMediaPreferencesScreen} from '#/screens/Settings/ExternalMediaPreferences' import {FindContactsSettingsScreen} from '#/screens/Settings/FindContactsSettings' @@ -405,6 +406,14 @@ function commonScreens(Stack: typeof Flat, unreadCountLabel?: string) { requireAuth: true, }} /> + BetaFeaturesSettingsScreen} + options={{ + title: title(msg`Beta features`), + requireAuth: true, + }} + /> AutomationLabelSettingsScreen} diff --git a/src/analytics/features/index.ts b/src/analytics/features/index.ts index 7ec6f359a7..6afd2f91b4 100644 --- a/src/analytics/features/index.ts +++ b/src/analytics/features/index.ts @@ -1,8 +1,11 @@ import {MMKV} from '@bsky.app/react-native-mmkv' import {setPolyfills} from '@growthbook/growthbook' import {GrowthBook} from '@growthbook/growthbook-react' +import {type I18n} from '@lingui/core' +import {msg} from '@lingui/core/macro' import {Logger} from '#/logger' +import {Features} from '#/analytics/features/types' import {getNavigationMetadata, type Metadata} from '#/analytics/metadata' import * as env from '#/env' @@ -11,12 +14,14 @@ export {Features} from '#/analytics/features/types' const logger = Logger.create(Logger.Context.Growthbook) const CACHE = new MMKV({id: 'bsky_features_cache'}) +const BETA_USER_ATTRIBUTE = 'isBetaUser' + setPolyfills({ localStorage: { getItem: key => { return CACHE.getString(key) ?? null }, - setItem: async (key, value) => { + setItem: (key, value) => { CACHE.set(key, value) }, }, @@ -44,15 +49,13 @@ export const features = new GrowthBook({ * that case, we may see a flash of uncustomized content until the * initialization completes. */ -export const init = new Promise(async y => { - const res = await features.init({timeout: TIMEOUT_INIT}) +export const init = features.init({timeout: TIMEOUT_INIT}).then(res => { if (!res.success) { logger.warn('GrowthBook initialization failed or timed out', { source: res.source, safeMessage: res.error?.toString(), }) } - y() }) /** @@ -68,6 +71,89 @@ export async function refresh({strategy}: {strategy: FeatureFetchStrategy}) { }) } +export function getFeatures() { + return features.getFeatures() +} + +export function getFeatureDescription(feature: Features, i18n: I18n) { + switch (feature) { + case Features.PostThreadKnownLikersEnable: + return { + key: feature, + name: i18n._( + msg({ + message: 'Social proofing on posts', + comment: 'Name for a feature flag', + }), + ), + description: i18n._( + msg({ + message: 'Spot posts your friends and follows have liked.', + comment: 'Description of a feature flag (Social proofing on posts)', + }), + ), + } + default: + return null + } +} + +/** + * Walks a GrowthBook condition tree to determine whether it targets the given + * attribute. Conditions can nest via the logical operators `$and`, `$or`, + * `$nor` (arrays of sub-conditions) and `$not` (a single sub-condition), so a + * flat scan of the top-level keys would miss e.g. + * `{$and: [{isBetaUser: true}, ...]}`. Dot-notation access (e.g. + * `isBetaUser.foo`) counts as targeting the attribute as well. + */ +function conditionTargetsAttribute( + condition: unknown, + attribute: string, +): boolean { + if (!condition || typeof condition !== 'object') return false + + for (const [key, value] of Object.entries(condition)) { + if (key === attribute || key.startsWith(`${attribute}.`)) return true + + if (key === '$and' || key === '$or' || key === '$nor') { + if ( + Array.isArray(value) && + value.some(sub => conditionTargetsAttribute(sub, attribute)) + ) { + return true + } + } else if (key === '$not') { + if (conditionTargetsAttribute(value, attribute)) return true + } + } + + return false +} + +export function getTargetedFeatures(i18n: I18n) { + const allFeatures = features.getFeatures() + const targetedFeatures: {key: Features; name: string; description: string}[] = + [] + for (const [featureKey, feature] of Object.entries(allFeatures)) { + // Check if the feature contains any rules + if (!feature.rules) continue + + // Determine if any rule targets the beta user attribute + const hasTargeting = feature.rules.some(rule => + conditionTargetsAttribute(rule.condition, BETA_USER_ATTRIBUTE), + ) + + if (hasTargeting) { + const featureName = getFeatureDescription(featureKey as Features, i18n) + if (featureName) { + targetedFeatures.push(featureName) + } + } + } + + return targetedFeatures +} + /** * Converts our metadata into GrowthBook attributes and sets them. GrowthBook * attributes are manually configured in the GrowthBook dashboard. So these @@ -80,7 +166,7 @@ export function setAttributes({ session, preferences, }: Metadata) { - features.setAttributes({ + void features.setAttributes({ deviceId: base.deviceId, sessionId: base.sessionId, platform: base.platform, @@ -92,5 +178,6 @@ export function setAttributes({ appLanguage: preferences?.appLanguage, contentLanguages: preferences?.contentLanguages, currentScreen: getNavigationMetadata()?.currentScreen, + isBetaUser: base.isBetaUser, }) } diff --git a/src/analytics/features/types.ts b/src/analytics/features/types.ts index b890873a37..2e754d8f2e 100644 --- a/src/analytics/features/types.ts +++ b/src/analytics/features/types.ts @@ -1,3 +1,7 @@ +/** + * If a feature is in the beta program, be sure to add a localized description + * for it via getFeatureDescription(). + */ export enum Features { // core flags IsBskyTeam = 'is_bsky_team', diff --git a/src/analytics/index.tsx b/src/analytics/index.tsx index b40b722095..20f919f501 100644 --- a/src/analytics/index.tsx +++ b/src/analytics/index.tsx @@ -1,4 +1,10 @@ -import {createContext, useContext, useMemo} from 'react' +import { + createContext, + useCallback, + useContext, + useMemo, + useSyncExternalStore, +} from 'react' import {Platform} from 'react-native' import {type Result} from '@growthbook/growthbook-react' @@ -26,7 +32,7 @@ import {type Metrics, metrics} from '#/analytics/metrics' import * as refParams from '#/analytics/misc/refParams' import * as env from '#/env' import {useGeolocationServiceResponse} from '#/geolocation/service' -import {device} from '#/storage' +import {account, device} from '#/storage' export * as utils from '#/analytics/utils' export const features = {init, refresh} @@ -120,6 +126,38 @@ Context.displayName = 'AnalyticsContext' */ export const setupDeviceId = getAndMigrateDeviceId() +/** + * Reads the per-account cached `isBetaUser` flag for `did`, kept in sync with + * writes from `BetaUserStorageSync` and the beta settings toggle. + * + * This deliberately does not use `useStorage`, whose `useState` seeds once and + * only updates via the change listener. The consuming `AnalyticsContext` lives + * above the `` remount breaker, so on an account switch it + * re-renders (with a new did) rather than remounting. `useStorage` would keep + * serving the previous account's seeded value until a write happened to fire + * its listener, leaking a beta account's flag into a non-beta account. Reading + * via `useSyncExternalStore` re-evaluates `getSnapshot` every render, so the + * value is always correct for the current did. + */ +function useAccountIsBetaUser(did: string | undefined): boolean | undefined { + const subscribe = useCallback( + (onChange: () => void) => { + if (!did) return () => {} + const sub = account.addOnValueChangedListener( + [did, 'isBetaUser'], + onChange, + ) + return () => sub.remove() + }, + [did], + ) + const getSnapshot = useCallback(() => { + if (!did) return undefined + return account.get([did, 'isBetaUser']) + }, [did]) + return useSyncExternalStore(subscribe, getSnapshot) +} + /** * Analytics context provider. Decorates the parent analytics context with * additional metadata. Nesting should be done carefully and sparingly. @@ -141,6 +179,16 @@ export function AnalyticsContext({ const sessionId = useSessionId() const geolocation = useGeolocationServiceResponse() const parentContext = useContext(Context) + /* + * `isBetaUser` is account-specific, so it's cached per account. Read it + * scoped to the did for this render's session (from the `metadata` prop when + * set, otherwise inherited from the parent context). Without a did (e.g. + * logged out, or the top-level context above the session provider) there's + * no value, so beta-gated features are never evaluated for an ineligible or + * absent account. + */ + const did = metadata?.session?.did ?? parentContext.metadata.session?.did + const isBetaUser = useAccountIsBetaUser(did) const childContext = useMemo(() => { const combinedMetadata = { ...parentContext.metadata, @@ -148,6 +196,7 @@ export function AnalyticsContext({ base: { ...parentContext.metadata.base, sessionId, + isBetaUser, }, geolocation, } @@ -166,7 +215,7 @@ export function AnalyticsContext({ }, } return context - }, [sessionId, geolocation, parentContext, metadata]) + }, [parentContext, metadata, sessionId, isBetaUser, geolocation]) return {children} } diff --git a/src/analytics/metadata.ts b/src/analytics/metadata.ts index 9039c38686..7077f58254 100644 --- a/src/analytics/metadata.ts +++ b/src/analytics/metadata.ts @@ -9,6 +9,7 @@ export type BaseMetadata = { bundleDate: number referrerSrc: string referrerUrl: string + isBetaUser?: boolean } export type GeolocationMetadata = Geolocation @@ -66,7 +67,7 @@ export function getMetadataForLogger({ base, geolocation, session, -}: Metadata): Record { +}: Metadata): Record { return { deviceId: base.deviceId, sessionId: base.sessionId, diff --git a/src/lib/routes/types.ts b/src/lib/routes/types.ts index b197d85bef..6eb112a381 100644 --- a/src/lib/routes/types.ts +++ b/src/lib/routes/types.ts @@ -63,6 +63,7 @@ export type CommonNavigatorParams = { PreferencesExternalEmbeds: undefined AccessibilitySettings: undefined AppearanceSettings: undefined + BetaFeaturesSettings: undefined AccountSettings: undefined AutomationLabelSettings: undefined PrivacyAndSecuritySettings: undefined diff --git a/src/routes.ts b/src/routes.ts index 87b58e0d07..3e45f1b379 100644 --- a/src/routes.ts +++ b/src/routes.ts @@ -50,6 +50,7 @@ export const router = new Router({ PreferencesExternalEmbeds: '/settings/external-embeds', AccessibilitySettings: '/settings/accessibility', AppearanceSettings: '/settings/appearance', + BetaFeaturesSettings: '/settings/beta-features', SavedFeeds: '/settings/saved-feeds', AccountSettings: '/settings/account', AutomationLabelSettings: '/settings/automation-label', diff --git a/src/screens/Settings/AccountSettings.tsx b/src/screens/Settings/AccountSettings.tsx index ed72c78a9f..94f3f6c9df 100644 --- a/src/screens/Settings/AccountSettings.tsx +++ b/src/screens/Settings/AccountSettings.tsx @@ -1,6 +1,4 @@ -import {msg} from '@lingui/core/macro' -import {useLingui} from '@lingui/react' -import {Trans} from '@lingui/react/macro' +import {Trans, useLingui} from '@lingui/react/macro' import {type NativeStackScreenProps} from '@react-navigation/native-stack' import {type CommonNavigatorParams} from '#/lib/routes/types' @@ -36,7 +34,7 @@ import {ExportCarDialog} from './components/ExportCarDialog' type Props = NativeStackScreenProps export function AccountSettingsScreen({}: Props) { const t = useTheme() - const {_} = useLingui() + const {t: l} = useLingui() const {currentAccount} = useSession() const {data: profile} = useProfileQuery({did: currentAccount?.did}) const emailDialogControl = useEmailDialogControl() @@ -83,7 +81,7 @@ export function AccountSettingsScreen({}: Props) { {currentAccount && !currentAccount.emailConfirmed && ( emailDialogControl.open({ id: EmailDialogScreenID.Verify, @@ -109,7 +107,7 @@ export function AccountSettingsScreen({}: Props) { )} emailDialogControl.open({ id: EmailDialogScreenID.Update, @@ -123,7 +121,7 @@ export function AccountSettingsScreen({}: Props) { changePasswordControl.open()}> @@ -132,8 +130,8 @@ export function AccountSettingsScreen({}: Props) { changeHandleControl.open()}> @@ -147,27 +145,27 @@ export function AccountSettingsScreen({}: Props) { Birthday birthdayControl.open()} /> + label={l`Automation label`}> Automation label {profile && ( - {isBotAccount(profile) ? _(msg`On`) : _(msg`Off`)} + {isBotAccount(profile) ? l`On` : l`Off`} )} exportCarControl.open()}> @@ -176,7 +174,7 @@ export function AccountSettingsScreen({}: Props) { deactivateAccountControl.open()} destructive> @@ -186,7 +184,7 @@ export function AccountSettingsScreen({}: Props) { deleteAccountControl.open()} destructive> @@ -197,7 +195,6 @@ export function AccountSettingsScreen({}: Props) { - diff --git a/src/screens/Settings/BetaFeaturesSettings.tsx b/src/screens/Settings/BetaFeaturesSettings.tsx new file mode 100644 index 0000000000..fa755faf0a --- /dev/null +++ b/src/screens/Settings/BetaFeaturesSettings.tsx @@ -0,0 +1,233 @@ +import {useEffect, useState} from 'react' +import {View} from 'react-native' +import {Trans, useLingui} from '@lingui/react/macro' +import {type NativeStackScreenProps} from '@react-navigation/native-stack' + +import {type CommonNavigatorParams} from '#/lib/routes/types' +import {logger} from '#/logger' +import { + usePreferencesQuery, + useSetIsBetaUserMutation, +} from '#/state/queries/preferences' +import {useSession} from '#/state/session' +import {BetaFeaturesFeedbackDialog} from '#/screens/Settings/components/BetaFeaturesFeedbackDialog' +import * as SettingsList from '#/screens/Settings/components/SettingsList' +import {atoms as a, useTheme} from '#/alf' +import {Admonition} from '#/components/Admonition' +import {Button, ButtonIcon, ButtonText} from '#/components/Button' +import * as Dialog from '#/components/Dialog' +import * as Toggle from '#/components/forms/Toggle' +import {Beaker_Stroke2_Corner2_Rounded as BeakerIcon} from '#/components/icons/Beaker' +import {BubbleInfo_Stroke2_Corner2_Rounded as BubbleInfoIcon} from '#/components/icons/BubbleInfo' +import * as Layout from '#/components/Layout' +import * as Toast from '#/components/Toast' +import {Text} from '#/components/Typography' +import {features} from '#/analytics' +import {getTargetedFeatures} from '#/analytics/features' +import {IS_INTERNAL, IS_WEB} from '#/env' +import {account} from '#/storage' + +type Props = NativeStackScreenProps< + CommonNavigatorParams, + 'BetaFeaturesSettings' +> + +export function BetaFeaturesSettingsScreen({}: Props) { + const t = useTheme() + const {t: l, i18n} = useLingui() + const {data: preferences} = usePreferencesQuery() + const {currentAccount} = useSession() + const {mutateAsync: setIsBetaUser} = useSetIsBetaUserMutation() + const isBetaUser = preferences?.bskyAppState?.isBetaUser ?? false + const [isPending, setIsPending] = useState(false) + const feedbackControl = Dialog.useDialogControl() + + /* + * `getTargetedFeatures` reads the GrowthBook singleton synchronously, but + * gates arrive asynchronously (from `init` on mount, or `refresh` after a + * toggle). Hold the result in state and re-read whenever fresh gates land so + * the list reflects the latest evaluation instead of a stale render. + */ + const [betaFeatures, setBetaFeatures] = useState(() => + getTargetedFeatures(i18n), + ) + + useEffect(() => { + let cancelled = false + void features.refresh({strategy: 'prefer-fresh-gates'}).then(() => { + if (!cancelled) setBetaFeatures(getTargetedFeatures(i18n)) + }) + return () => { + cancelled = true + } + }, [i18n]) + + const onChange = async (next: boolean) => { + try { + setIsPending(true) + await setIsBetaUser(next) + /* + * Cache the new value so analytics can set the `isBetaUser` GrowthBook + * attribute synchronously on the next boot, before beta-gated features + * are evaluated. Scoped per account, since `isBetaUser` is + * account-specific. + */ + if (currentAccount) { + account.set([currentAccount.did, 'isBetaUser'], next) + } + } catch (e) { + logger.error('Failed to toggle beta features', {safeMessage: e}) + Toast.show(l`Something went wrong, please try again.`, {type: 'error'}) + return + } finally { + setIsPending(false) + } + /* + * The toggle already succeeded; re-evaluate feature gates against the new + * attribute in-session as a best-effort follow-up. A failure here should + * not surface as a toggle error. + */ + try { + await features.refresh({strategy: 'prefer-fresh-gates'}) + setBetaFeatures(getTargetedFeatures(i18n)) + } catch {} + } + + const onPressShareFeedback = () => { + feedbackControl.open() + } + + const canSubmitFeedback = + !isPending && isBetaUser && (betaFeatures.length > 0 || IS_INTERNAL) + + return ( + + + + + + Beta features + + + + + + + void onChange(value)}> + + + + Enable beta features + + + + Get early access to experimental features we’re testing. + + + + + + + + + + {IS_WEB + ? l({ + message: + 'Beta features may be unstable. Some changes may require reloading the app.', + context: 'web', + }) + : l({ + message: + 'Beta features may be unstable. Some changes may require restarting the app.', + context: 'native', + })} + + + + + {betaFeatures.length < 1 ? ( + + + + No beta features at the moment. + + + Check back later! + + + ) : ( + <> + + Current beta features + + + {betaFeatures.map(feature => ( + + + {feature.name} + + + {feature.description} + + + ))} + + + )} + + + + + feature.key)} + /> + + ) +} diff --git a/src/screens/Settings/Settings.tsx b/src/screens/Settings/Settings.tsx index 5fd53cde39..405f1136e6 100644 --- a/src/screens/Settings/Settings.tsx +++ b/src/screens/Settings/Settings.tsx @@ -36,6 +36,7 @@ import {useIsFindContactsFeatureEnabledBasedOnGeolocation} from '#/components/co import {useDialogControl} from '#/components/Dialog' import {SwitchAccountDialog} from '#/components/dialogs/SwitchAccount' import {Accessibility_Stroke2_Corner2_Rounded as AccessibilityIcon} from '#/components/icons/Accessibility' +import {Beaker_Stroke2_Corner2_Rounded as BeakerIcon} from '#/components/icons/Beaker' import {Bell_Stroke2_Corner0_Rounded as NotificationIcon} from '#/components/icons/Bell' import {BubbleInfo_Stroke2_Corner2_Rounded as BubbleInfoIcon} from '#/components/icons/BubbleInfo' import {ChevronTop_Stroke2_Corner0_Rounded as ChevronUpIcon} from '#/components/icons/Chevron' @@ -242,6 +243,14 @@ export function SettingsScreen({}: Props) { Languages + + + + Beta features + + void Linking.openURL(HELP_DESK_URL)} label={l`Help`} diff --git a/src/screens/Settings/components/BetaFeaturesFeedbackDialog.tsx b/src/screens/Settings/components/BetaFeaturesFeedbackDialog.tsx new file mode 100644 index 0000000000..1cca74d5ad --- /dev/null +++ b/src/screens/Settings/components/BetaFeaturesFeedbackDialog.tsx @@ -0,0 +1,159 @@ +import {useState} from 'react' +import {View} from 'react-native' +import {plural} from '@lingui/core/macro' +import {Trans, useLingui} from '@lingui/react/macro' +import {useMutation} from '@tanstack/react-query' + +import {logger} from '#/logger' +import {Sentry} from '#/logger/sentry/lib' +import {atoms as a, useTheme, web} from '#/alf' +import {Button, ButtonText} from '#/components/Button' +import * as Dialog from '#/components/Dialog' +import * as Toast from '#/components/Toast' +import {Text} from '#/components/Typography' + +const MAX_FEEDBACK_LENGTH = 300 + +export function BetaFeaturesFeedbackDialog({ + control, + betaFeatureKeys, +}: { + control: Dialog.DialogControlProps + /** + * The keys of the beta feature gates that are currently active for this user, + * attached to the feedback report for context. + */ + betaFeatureKeys: string[] +}) { + return ( + + + + + + ) +} + +function BetaFeaturesFeedbackDialogInner({ + betaFeatureKeys, +}: { + betaFeatureKeys: string[] +}) { + const t = useTheme() + const {t: l} = useLingui() + const control = Dialog.useDialogContext() + + const [feedback, setFeedback] = useState('') + + const {mutate: onSubmit, isPending} = useMutation({ + mutationFn: () => { + /* + * `captureFeedback` is a no-op when the Sentry client is disabled (e.g. in + * dev, where `init` sets `enabled: false`). It returns synchronously + * without sending or throwing, so guard here to route to the error toast + * instead of falsely reporting success. + */ + const enabled = Sentry.getClient()?.getOptions().enabled ?? false + if (!enabled) { + throw new Error('Sentry is disabled; feedback was not sent') + } + Sentry.captureFeedback( + { + message: feedback.trim(), + }, + { + captureContext: { + contexts: { + betaFeatures: {keys: betaFeatureKeys}, + }, + }, + }, + ) + return Promise.resolve() + }, + onSuccess: () => { + control.close(() => { + setFeedback('') + Toast.show(l`Thanks for your feedback!`) + }) + }, + onError: error => { + logger.error('Failed to send beta features feedback', { + safeMessage: error, + }) + Toast.show(l`Something went wrong, please try again.`, {type: 'error'}) + }, + }) + + const remaining = MAX_FEEDBACK_LENGTH - feedback.length + const canSubmit = feedback.trim().length > 0 && !isPending + + return ( + + + + + {l`Share feedback`} + + + {l`We’d love to hear about your experience testing beta features!`} + + + + + {remaining} + + + setFeedback(value.slice(0, MAX_FEEDBACK_LENGTH)) + } + /> + + + + + + + + ) +} diff --git a/src/state/preferences/beta-user-sync.tsx b/src/state/preferences/beta-user-sync.tsx new file mode 100644 index 0000000000..f8cead4955 --- /dev/null +++ b/src/state/preferences/beta-user-sync.tsx @@ -0,0 +1,37 @@ +import {useEffect} from 'react' + +import {usePreferencesQuery} from '#/state/queries/preferences' +import {useSession} from '#/state/session' +import {account} from '#/storage' + +/** + * Caches `bskyAppState.isBetaUser` from preferences into synchronous device + * storage so analytics can read it at init (before beta-gated features are + * evaluated). Scoped per account, since `isBetaUser` is account-specific: + * a global cache would let one account's value leak into another after a + * switch, until the new account's preferences loaded. Must be mounted below + * `QueryProvider`, since the analytics providers that consume the cached value + * sit above it. + * + * Lives outside `#/analytics` so that module never imports the preferences + * query, which would create a circular import. + */ +export function BetaUserStorageSync() { + const {currentAccount} = useSession() + const did = currentAccount?.did + const {data: preferences} = usePreferencesQuery() + const isBetaUser = preferences?.bskyAppState?.isBetaUser + + useEffect(() => { + if (!did) return + if (isBetaUser === undefined) return + /* + * Guard against a redundant write on every warm boot. Writing triggers the + * storage change listener, which re-renders the analytics subtree. + */ + if (account.get([did, 'isBetaUser']) === isBetaUser) return + account.set([did, 'isBetaUser'], isBetaUser) + }, [did, isBetaUser]) + + return null +} diff --git a/src/state/queries/preferences/const.ts b/src/state/queries/preferences/const.ts index 02f7d62a87..74c86d1020 100644 --- a/src/state/queries/preferences/const.ts +++ b/src/state/queries/preferences/const.ts @@ -50,6 +50,7 @@ export const DEFAULT_LOGGED_OUT_PREFERENCES: UsePreferencesQueryResponse = { queuedNudges: [], activeProgressGuide: undefined, nuxs: [], + isBetaUser: undefined, }, postInteractionSettings: { threadgateAllowRules: undefined, diff --git a/src/state/queries/preferences/index.ts b/src/state/queries/preferences/index.ts index 816683062e..41adfc9a04 100644 --- a/src/state/queries/preferences/index.ts +++ b/src/state/queries/preferences/index.ts @@ -438,6 +438,21 @@ export function useSetActiveProgressGuideMutation() { }) } +export function useSetIsBetaUserMutation() { + const queryClient = useQueryClient() + const agent = useAgent() + + return useMutation({ + mutationFn: async (isBetaUser: boolean) => { + await agent.setIsBetaUser(isBetaUser) + // triggers a refetch + await queryClient.invalidateQueries({ + queryKey: preferencesQueryKey, + }) + }, + }) +} + export function useSetVerificationPrefsMutation() { const ax = useAnalytics() const queryClient = useQueryClient() diff --git a/src/storage/schema.ts b/src/storage/schema.ts index add2e9a0a8..33c6bc0b56 100644 --- a/src/storage/schema.ts +++ b/src/storage/schema.ts @@ -91,4 +91,16 @@ export type Account = { * Recently selected GIFs in the GIF picker. Most recent first, capped at 20. */ recentGifs?: Gif[] + + /** + * Cached from preferences (`bskyAppState.isBetaUser`) so the GrowthBook + * `isBetaUser` attribute can be set synchronously at analytics init, before + * beta-gated features (e.g. SearchV2Enable) are first evaluated. Written back + * when preferences load. + * + * Scoped per account, since `isBetaUser` is account-specific preference data. + * Reading it globally would let a beta account's value leak into a non-beta + * account after a switch, until that account's preferences loaded. + */ + isBetaUser?: boolean } From 5b9967b17d51c7485cf6bf5ca10a9807632fa721 Mon Sep 17 00:00:00 2001 From: DS Boyce <260543580+ds-boyce@users.noreply.github.com> Date: Wed, 15 Jul 2026 09:23:11 -0700 Subject: [PATCH 33/52] Strip leading @ in from: queries (#11158) --- .../__tests__/search-posts-params.test.ts | 17 +++++++++++++++++ src/state/queries/search-posts-params.ts | 15 +++++++++++++-- 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/src/state/queries/__tests__/search-posts-params.test.ts b/src/state/queries/__tests__/search-posts-params.test.ts index 9436507cc2..9b0c374642 100644 --- a/src/state/queries/__tests__/search-posts-params.test.ts +++ b/src/state/queries/__tests__/search-posts-params.test.ts @@ -26,6 +26,23 @@ describe(`extractSearchPostsParams`, () => { input: `cats to:alice`, output: {q: `cats`, mentions: `alice`}, }, + // Handles may be typed with a leading @ (e.g. from:@alice.bsky.social), + // which the appview rejects. Strip it to match the advanced-search dialog. + { + name: `strips a leading @ from from:`, + input: `cats from:@alice.bsky.social`, + output: {q: `cats`, author: `alice.bsky.social`}, + }, + { + name: `strips a leading @ from mentions:`, + input: `cats mentions:@alice.bsky.social`, + output: {q: `cats`, mentions: `alice.bsky.social`}, + }, + { + name: `strips a leading @ from to: (mentions alias)`, + input: `cats to:@alice.bsky.social`, + output: {q: `cats`, mentions: `alice.bsky.social`}, + }, // `me` is resolved by the backend, so the :me operators stay in q verbatim // instead of being lifted into author/mentions. { diff --git a/src/state/queries/search-posts-params.ts b/src/state/queries/search-posts-params.ts index 70dfe728ba..dcfd9aa4b5 100644 --- a/src/state/queries/search-posts-params.ts +++ b/src/state/queries/search-posts-params.ts @@ -13,6 +13,17 @@ import { const DATE_RE = /^\d{4}-\d{2}-\d{2}/ +/** + * Strips a leading `@` from a handle so `from:@alice.bsky.social` and + * `from:alice.bsky.social` both resolve to the same author. Mirrors the marker + * stripping the advanced-search dialog applies to handles entered in its + * filter fields (see `serializeAdvancedSearch`), which otherwise 400s the + * appview. + */ +function stripHandleMarker(value: string): string { + return value.startsWith('@') ? value.slice(1) : value +} + export type ExtractedSearchParams = { q: string author?: string @@ -111,12 +122,12 @@ export function extractSearchPostsParams(query: string): ExtractedSearchParams { * query text verbatim rather than lifting it into a structured param. */ if (value === 'me') remaining.push(token) - else result.author ??= value + else result.author ??= stripHandleMarker(value) break case 'mentions': case 'to': if (value === 'me') remaining.push(token) - else result.mentions ??= value + else result.mentions ??= stripHandleMarker(value) break case 'domain': result.domain ??= value From 211c8838a9f8312110d236e3da3887edf03aedfe Mon Sep 17 00:00:00 2001 From: DS Boyce <260543580+ds-boyce@users.noreply.github.com> Date: Wed, 15 Jul 2026 11:15:34 -0700 Subject: [PATCH 34/52] Upgrade pnpm to 11.13.0 (#11163) --- package.json | 2 +- pnpm-lock.yaml | 76 +++++++++++++++++++++++++------------------------- 2 files changed, 39 insertions(+), 39 deletions(-) diff --git a/package.json b/package.json index 2542931936..a108d02a09 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ "devEngines": { "packageManager": { "name": "pnpm", - "version": "11.11.0", + "version": "11.13.0", "onFail": "warn" }, "runtime": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 51468f2236..a908d5d1c8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -7,52 +7,52 @@ importers: configDependencies: {} packageManagerDependencies: '@pnpm/exe': - specifier: 11.11.0 - version: 11.11.0 + specifier: 11.13.0 + version: 11.13.0 pnpm: - specifier: 11.11.0 - version: 11.11.0 + specifier: 11.13.0 + version: 11.13.0 packages: - '@pnpm/exe@11.11.0': - resolution: {integrity: sha512-OpTrbkAU0Ur8gBwhAT6mbWwlA1bFU8zPcFNdp/img/RqFIc7Dn0j0crW9rz5hxTWo/UBQjy6Sb9jh+N5bRSd+g==} + '@pnpm/exe@11.13.0': + resolution: {integrity: sha512-aXsm4VW1awRuh4X4G4fl+1xdV+L9DElKrgc4Yq/3zgGQQ12ATj1n3MMeosfa68aX/i7E9VyWT42adDSg8+kEFQ==} hasBin: true - '@pnpm/linux-arm64@11.11.0': - resolution: {integrity: sha512-62K/kQY3jaoQ96MNUi0NLcTSSDO2QrtziOA2IK5R/m+DxpSwXbxFSQHYx8oOg0r1+MFVqwuDkFjXe6DyW3y58g==} + '@pnpm/linux-arm64@11.13.0': + resolution: {integrity: sha512-86ri9P/i3Enbv0NBAEtz27lBHHwCJqsC7YxHX56+jXPyFobGbsDVR5OUyoCCVVNz/Phs2O7c9SaDVdqKPzUFCA==} cpu: [arm64] os: [linux] - '@pnpm/linux-x64@11.11.0': - resolution: {integrity: sha512-rwMbNJR+PstRu+ymWoApei1CWrAnsnW3tm+3H8qOxbp8duiaj6u7DxlMzhKbVpFwylxcJdeGwZ5tReBFOVpsdw==} + '@pnpm/linux-x64@11.13.0': + resolution: {integrity: sha512-ZXvUMP5ew1WZllTq1f7Tm3l6sF5LK5sMesfdu9J6uxsznK/3tTi6edUeiNN6dA22mAHhjvrzVj+MeQ6X3DG0tA==} cpu: [x64] os: [linux] - '@pnpm/linuxstatic-arm64@11.11.0': - resolution: {integrity: sha512-OcmrMw1hxNee7KTBpE0yToTZziH/SCmJWwjB7YN2NmsxZVPKM2vtOWFdZufW0YN5JffKMaHbPZ2ulgYBM5qAQw==} + '@pnpm/linuxstatic-arm64@11.13.0': + resolution: {integrity: sha512-elwRj0sE3W54gDECgbHu2/TerpZsHXRmLSXfzuIvReumGuOYk5zlUhD5dJqzp0B1ktetVNU4Rb1keENz5/Gb/Q==} cpu: [arm64] os: [linux] libc: [musl] - '@pnpm/linuxstatic-x64@11.11.0': - resolution: {integrity: sha512-pWeAYeS+PPah6mXcJbOr+nPwEpyQau4iPcsqNUhTK9G5/qpG5dU1m8oHeIH83PuUUvvdJddLE1PXUkGM+QCI6g==} + '@pnpm/linuxstatic-x64@11.13.0': + resolution: {integrity: sha512-PQyfFMRA6/uYB+1vqx0wSXfOirC5CUOKRlTjXNgI1q/R1Ug/NMDfKOr52qbWFllcG9pIHKV8hZTo4WyBUjS1mA==} cpu: [x64] os: [linux] libc: [musl] - '@pnpm/macos-arm64@11.11.0': - resolution: {integrity: sha512-62P8Pe4yvMkdl2nxswCjM5X835i9Judk68CZvv8OnWsm7CVCEZ61SEBnNTpjJ4kXzdayFjRpPwE/jfJSvPaWww==} + '@pnpm/macos-arm64@11.13.0': + resolution: {integrity: sha512-qWf2wbvESMICLJcVfWNh4pwIsr2oK6yFwbuv1uHi0UY3OnWNt0+POmhqWtYWtDj+EPWM3pSfEAc+KFQAiaBCvg==} cpu: [arm64] os: [darwin] - '@pnpm/win-arm64@11.11.0': - resolution: {integrity: sha512-yYAx+A1oT+mSgrIzysuAvdYYMYEfAa+z3/UCGY3L5VC9oabs9qi71LbTan1cDui/AadrIvD177k9mV4V38KAJg==} + '@pnpm/win-arm64@11.13.0': + resolution: {integrity: sha512-ZvbGFdjU3cX32+/sHOhV9VkkbBucJ9QyW/F6VSsqgkvmTZTXlQglYp4K1KBbKWxYKz4i6M9p5PStI6yYB+y0jA==} cpu: [arm64] os: [win32] - '@pnpm/win-x64@11.11.0': - resolution: {integrity: sha512-ehTuyM5Rrp4ye0SdtAJkUS+9ykfwdDY9SPqiK3+bxXPx3LD5aeDw2EBksTh/xRTgqFdYmoFR86cABFt9yBr0GA==} + '@pnpm/win-x64@11.13.0': + resolution: {integrity: sha512-5KX2lMFbXZNvKC+gLftR5FiU25j/jMp24Zi9JB01T3r42uxrHt12GQP7CNkNjxCXdi8jioUB7e3iT1eINS8qVQ==} cpu: [x64] os: [win32] @@ -116,45 +116,45 @@ packages: resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} engines: {node: '>=8'} - pnpm@11.11.0: - resolution: {integrity: sha512-RGP2X9gO2A1pvB1L8WPulPYFxzgPwxi7Wy6+FfjNEtScUaTVnpUbQB52TTtsp1HL9RvFDtcAGmvLSTXmhMNIgg==} + pnpm@11.13.0: + resolution: {integrity: sha512-iNlHJNjy5sGGdEpVhMblnsrIaex7oV6ctM1ijo3HBmggskgdjuO1HqjaMjpzeAaKpYxVajcg0yt8IKBR0Ig2Og==} engines: {node: '>=22.13'} hasBin: true snapshots: - '@pnpm/exe@11.11.0': + '@pnpm/exe@11.13.0': dependencies: '@reflink/reflink': 0.1.19 detect-libc: 2.1.2 optionalDependencies: - '@pnpm/linux-arm64': 11.11.0 - '@pnpm/linux-x64': 11.11.0 - '@pnpm/linuxstatic-arm64': 11.11.0 - '@pnpm/linuxstatic-x64': 11.11.0 - '@pnpm/macos-arm64': 11.11.0 - '@pnpm/win-arm64': 11.11.0 - '@pnpm/win-x64': 11.11.0 + '@pnpm/linux-arm64': 11.13.0 + '@pnpm/linux-x64': 11.13.0 + '@pnpm/linuxstatic-arm64': 11.13.0 + '@pnpm/linuxstatic-x64': 11.13.0 + '@pnpm/macos-arm64': 11.13.0 + '@pnpm/win-arm64': 11.13.0 + '@pnpm/win-x64': 11.13.0 - '@pnpm/linux-arm64@11.11.0': + '@pnpm/linux-arm64@11.13.0': optional: true - '@pnpm/linux-x64@11.11.0': + '@pnpm/linux-x64@11.13.0': optional: true - '@pnpm/linuxstatic-arm64@11.11.0': + '@pnpm/linuxstatic-arm64@11.13.0': optional: true - '@pnpm/linuxstatic-x64@11.11.0': + '@pnpm/linuxstatic-x64@11.13.0': optional: true - '@pnpm/macos-arm64@11.11.0': + '@pnpm/macos-arm64@11.13.0': optional: true - '@pnpm/win-arm64@11.11.0': + '@pnpm/win-arm64@11.13.0': optional: true - '@pnpm/win-x64@11.11.0': + '@pnpm/win-x64@11.13.0': optional: true '@reflink/reflink-darwin-arm64@0.1.19': @@ -194,7 +194,7 @@ snapshots: detect-libc@2.1.2: {} - pnpm@11.11.0: {} + pnpm@11.13.0: {} --- lockfileVersion: '9.0' From 7009417035a805183c907262664e78915af45258 Mon Sep 17 00:00:00 2001 From: DS Boyce <260543580+ds-boyce@users.noreply.github.com> Date: Wed, 15 Jul 2026 11:21:07 -0700 Subject: [PATCH 35/52] Remove IS_INTERNAL check for beta feedback button (#11162) --- src/screens/Settings/BetaFeaturesSettings.tsx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/screens/Settings/BetaFeaturesSettings.tsx b/src/screens/Settings/BetaFeaturesSettings.tsx index fa755faf0a..daa870bd81 100644 --- a/src/screens/Settings/BetaFeaturesSettings.tsx +++ b/src/screens/Settings/BetaFeaturesSettings.tsx @@ -24,7 +24,7 @@ import * as Toast from '#/components/Toast' import {Text} from '#/components/Typography' import {features} from '#/analytics' import {getTargetedFeatures} from '#/analytics/features' -import {IS_INTERNAL, IS_WEB} from '#/env' +import {IS_WEB} from '#/env' import {account} from '#/storage' type Props = NativeStackScreenProps< @@ -97,8 +97,7 @@ export function BetaFeaturesSettingsScreen({}: Props) { feedbackControl.open() } - const canSubmitFeedback = - !isPending && isBetaUser && (betaFeatures.length > 0 || IS_INTERNAL) + const canSubmitFeedback = !isPending && isBetaUser && betaFeatures.length > 0 return ( From a0c75944a36e98893b6c77929b209f5c7d57e37c Mon Sep 17 00:00:00 2001 From: DS Boyce <260543580+ds-boyce@users.noreply.github.com> Date: Wed, 15 Jul 2026 11:27:19 -0700 Subject: [PATCH 36/52] Don't suggest retrying an unretryable error (#11159) --- oxlint-suppressions.json | 11 ----------- src/lib/strings/errors.ts | 15 ++++++++++----- src/screens/Search/SearchResults.tsx | 18 +++++++++++++++--- 3 files changed, 25 insertions(+), 19 deletions(-) diff --git a/oxlint-suppressions.json b/oxlint-suppressions.json index b5eb42488b..f846bb0561 100644 --- a/oxlint-suppressions.json +++ b/oxlint-suppressions.json @@ -866,17 +866,6 @@ "count": 1 } }, - "src/lib/strings/errors.ts": { - "typescript/no-explicit-any": { - "count": 1 - }, - "typescript/no-unsafe-call": { - "count": 14 - }, - "typescript/no-unsafe-member-access": { - "count": 14 - } - }, "src/lib/useGetEmojis/getEmojis.ts": { "typescript/require-await": { "count": 1 diff --git a/src/lib/strings/errors.ts b/src/lib/strings/errors.ts index 5f56cccc4d..994c91515b 100644 --- a/src/lib/strings/errors.ts +++ b/src/lib/strings/errors.ts @@ -1,13 +1,12 @@ import {XRPCError} from '@atproto/api' import {t} from '@lingui/core/macro' -export function cleanError(str: any): string { - if (!str) { +export function cleanError(e: unknown): string { + if (!e) { return '' } - if (typeof str !== 'string') { - str = str.toString() - } + // oxlint-disable-next-line typescript/no-base-to-string + const str = typeof e === 'string' ? e : e.toString() if (isNetworkError(str)) { return t`Unable to connect. Please check your internet connection and try again.` } @@ -86,3 +85,9 @@ export function isCancelledError(e: unknown) { const str = String(e).toLowerCase() return str.includes('cancel') } + +// TODO Replace this with error.shouldRetry() when available. -dsb +const RETRYABLE_ERRORS = [408, 425, 429, 500, 502, 503, 504, 522, 524] +export function shouldRetryError(e: unknown) { + return e instanceof XRPCError && RETRYABLE_ERRORS.includes(e.status) +} diff --git a/src/screens/Search/SearchResults.tsx b/src/screens/Search/SearchResults.tsx index 8d1377be33..70755e8148 100644 --- a/src/screens/Search/SearchResults.tsx +++ b/src/screens/Search/SearchResults.tsx @@ -6,7 +6,11 @@ import {Trans, useLingui} from '@lingui/react/macro' import {urls} from '#/lib/constants' import {usePostViewTracking} from '#/lib/hooks/usePostViewTracking' import {useCallOnce} from '#/lib/once' -import {cleanError} from '#/lib/strings/errors' +import { + cleanError, + isNetworkError, + shouldRetryError, +} from '#/lib/strings/errors' import {augmentSearchQuery} from '#/lib/strings/helpers' import {useActorSearch} from '#/state/queries/actor-search' import {usePopularFeedsSearch} from '#/state/queries/feed' @@ -402,7 +406,11 @@ let SearchScreenPostResults = ({ return error ? ( ) : ( @@ -539,7 +547,11 @@ let SearchScreenUserResults = ({ if (error) { return ( ) From 48fecae6771ee3e4a8731fb58cc4a932a8c1d890 Mon Sep 17 00:00:00 2001 From: DS Boyce <260543580+ds-boyce@users.noreply.github.com> Date: Wed, 15 Jul 2026 12:52:53 -0700 Subject: [PATCH 37/52] Log additional context for post:* events (#11035) --- src/screens/PostThread/index.tsx | 164 +++++++++++++++++++------------ 1 file changed, 102 insertions(+), 62 deletions(-) diff --git a/src/screens/PostThread/index.tsx b/src/screens/PostThread/index.tsx index 64d6529728..bc2578b895 100644 --- a/src/screens/PostThread/index.tsx +++ b/src/screens/PostThread/index.tsx @@ -14,7 +14,11 @@ import {useInitialNumToRender} from '#/lib/hooks/useInitialNumToRender' import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback' import {useOpenComposer} from '#/lib/hooks/useOpenComposer' import {usePostViewTracking} from '#/lib/hooks/usePostViewTracking' -import {useFeedFeedback} from '#/state/feed-feedback' +import { + FeedFeedbackProvider, + type StateContext as FeedFeedbackStateContext, + useFeedFeedback, +} from '#/state/feed-feedback' import {type ThreadViewOption} from '#/state/queries/preferences/useThreadPreferences' import { PostThreadContextProvider, @@ -56,6 +60,9 @@ import {IS_NATIVE} from '#/env' const PARENT_CHUNK_SIZE = IS_NATIVE ? 5 : 20 const CHILDREN_CHUNK_SIZE = 50 +const analyticsOnlyOnItemSeen: FeedFeedbackStateContext['onItemSeen'] = () => {} +const analyticsOnlySendInteraction: FeedFeedbackStateContext['sendInteraction'] = + () => {} export function PostThread({uri}: {uri: string}) { const ax = useAnalytics() @@ -569,68 +576,71 @@ export function PostThread({uri}: {uri: string}) { onRetry={thread.actions.refetch} /> ) : ( - { - // Track post:view for parent posts and replies (non-anchor posts) - if (item.type === 'threadPost' && item.depth !== 0) { - trackThreadItemView(item.value.post) + + { + // Track post:view for parent posts and replies (non-anchor posts) + if (item.type === 'threadPost' && item.depth !== 0) { + trackThreadItemView(item.value.post) + } + }} + /** + * NATIVE ONLY + * {@link https://reactnative.dev/docs/scrollview#maintainvisiblecontentposition} + */ + maintainVisibleContentPosition={{minIndexForVisible: 0}} + desktopFixedHeight + sideBorders={false} + ListFooterComponent={ + } - }} - /** - * NATIVE ONLY - * {@link https://reactnative.dev/docs/scrollview#maintainvisiblecontentposition} - */ - maintainVisibleContentPosition={{minIndexForVisible: 0}} - desktopFixedHeight - sideBorders={false} - ListFooterComponent={ - - } - initialNumToRender={initialNumToRender} - /** - * Default: 21 - * - * Smaller for placeholder data so we don't waste time rendering skeletons - */ - windowSize={thread.state.isPlaceholderData ? 1 : 7} - /** - * Default: 10 - */ - maxToRenderPerBatch={5} - /** - * Default: 50 - */ - updateCellsBatchingPeriod={100} - /> + initialNumToRender={initialNumToRender} + /** + * Default: 21 + * + * Smaller for placeholder data so we don't waste time rendering skeletons + */ + windowSize={thread.state.isPlaceholderData ? 1 : 7} + /** + * Default: 10 + */ + maxToRenderPerBatch={5} + /** + * Default: 50 + */ + updateCellsBatchingPeriod={100} + /> + )} {!gtMobile && canReply && hasSession && ( @@ -640,6 +650,36 @@ export function PostThread({uri}: {uri: string}) { ) } +function AnalyticsOnlyFeedFeedbackProvider({ + children, + feedDescriptor, +}: React.PropsWithChildren<{ + feedDescriptor: FeedFeedbackStateContext['feedDescriptor'] +}>) { + /* + * Non-anchor posts (parents and replies) are not the post the user tapped + * from a feed, so they should not report feed interactions. But they should + * still carry the originating feedDescriptor for analytics, so events like + * post:clickQuotePost can be attributed to the feed the thread was opened + * from. This inert context value provides feedDescriptor while keeping + * interaction reporting disabled (enabled: false makes sendInteraction and + * onItemSeen no-ops). The anchor renders its own full provider that nests + * inside and overrides this for its subtree. + */ + const value = useMemo( + () => ({ + enabled: false, + onItemSeen: analyticsOnlyOnItemSeen, + sendInteraction: analyticsOnlySendInteraction, + feedDescriptor, + feedSourceInfo: undefined, + }), + [feedDescriptor], + ) + + return {children} +} + function MobileComposePrompt({onPressReply}: {onPressReply: () => unknown}) { const {footerHeight} = useShellLayout() From 4705b7f408753615d403de13619b9d8e48400248 Mon Sep 17 00:00:00 2001 From: RetroSunstar <57507616+RetroSunstar@users.noreply.github.com> Date: Wed, 15 Jul 2026 23:32:18 +0100 Subject: [PATCH 38/52] Add DM button to labeller profiles (#11149) --- src/screens/Profile/Header/ProfileHeaderLabeler.tsx | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/screens/Profile/Header/ProfileHeaderLabeler.tsx b/src/screens/Profile/Header/ProfileHeaderLabeler.tsx index 5ce90205c9..96e136da5c 100644 --- a/src/screens/Profile/Header/ProfileHeaderLabeler.tsx +++ b/src/screens/Profile/Header/ProfileHeaderLabeler.tsx @@ -25,6 +25,7 @@ import {ProfileMenu} from '#/view/com/profile/ProfileMenu' import {atoms as a, tokens, useTheme} from '#/alf' import {Button, ButtonText} from '#/components/Button' import {type DialogOuterProps, useDialogControl} from '#/components/Dialog' +import {MessageProfileButton} from '#/components/dms/MessageProfileButton' import { Heart2_Filled_Stroke2_Corner0_Rounded as HeartFilled, Heart2_Stroke2_Corner0_Rounded as Heart, @@ -240,7 +241,7 @@ export function HeaderLabelerButtons({ const t = useTheme() const ax = useAnalytics() const {_} = useLingui() - const {currentAccount} = useSession() + const {currentAccount, hasSession} = useSession() const requireAuth = useRequireAuth() const playHaptic = useHaptics() const editProfileControl = useDialogControl() @@ -286,6 +287,11 @@ export function HeaderLabelerButtons({ }) return ( <> + {hasSession && + !isMe && + !profile.viewer?.blockedBy && + !profile.viewer?.blocking && } + {isMe ? ( <>