diff --git a/e2e/desktop.spec.ts b/e2e/desktop.spec.ts index 374676b..b12ed90 100644 --- a/e2e/desktop.spec.ts +++ b/e2e/desktop.spec.ts @@ -6,10 +6,10 @@ test.use(devices["Desktop Chrome"]); test.describe("Given a desktop browser", async () => { test.describe("When opening a Publication link", async () => { - test("Then it should omit mobile apps options", async ({ textPost }) => { + test("Then it should omit mobile-only apps", async ({ textPost }) => { await textPost.open(); - await expect(textPost.options).not.toHaveText(["Orb", "Phaver", "Buttrfly"]); + await expect(textPost.options).not.toHaveText(["Phaver"]); }); }); }); diff --git a/e2e/fixtures/profiles.ts b/e2e/fixtures/profiles.ts index e916ba9..82bd907 100644 --- a/e2e/fixtures/profiles.ts +++ b/e2e/fixtures/profiles.ts @@ -3,20 +3,15 @@ import { test as base, expect } from "@playwright/test"; import { ProfilePage } from "./ProfilePage"; export const test = base.extend<{ - v1Profile: ProfilePage; - v1ProfileWithSuffix: ProfilePage; - v2Profile: ProfilePage; + lensProfile: ProfilePage; + anyProfile: ProfilePage; }>({ - v1Profile: async ({ page }, use) => { - const profile = new ProfilePage(page, "lensprotocol"); + lensProfile: async ({ page }, use) => { + const profile = new ProfilePage(page, "lens/lens"); await use(profile); }, - v1ProfileWithSuffix: async ({ page }, use) => { - const profile = new ProfilePage(page, "lensprotocol.lens"); - await use(profile); - }, - v2Profile: async ({ page }, use) => { - const profile = new ProfilePage(page, "lens/lensprotocol"); + anyProfile: async ({ page }, use) => { + const profile = new ProfilePage(page, "lens/stani"); await use(profile); }, }); diff --git a/e2e/mobile.spec.ts b/e2e/mobile.spec.ts index 1ad48c6..1f6df8b 100644 --- a/e2e/mobile.spec.ts +++ b/e2e/mobile.spec.ts @@ -9,7 +9,7 @@ test.describe("Given a mobile browser", async () => { test("Then it should show in order mobile and web apps options", async ({ textPost }) => { await textPost.open(); - await expect(textPost.options).toHaveText(["Buttrfly", "Orb", "Hey", "Lensta", "Soclly"]); + await expect(textPost.options).toHaveText(["Buttrfly", "Hey", "orb", "Soclly"]); }); }); }); diff --git a/e2e/profiles.spec.ts b/e2e/profiles.spec.ts index 7885b5f..ee50071 100644 --- a/e2e/profiles.spec.ts +++ b/e2e/profiles.spec.ts @@ -6,15 +6,14 @@ test.use(devices["Desktop Chrome"]); test.describe("Given a Profile link", async () => { test.describe("When opening it", async () => { - test("Then it should show relevant app options", async ({ v1Profile }) => { - await v1Profile.open(); + test("Then it should show relevant app options", async ({ anyProfile }) => { + await anyProfile.open(); - await expect(v1Profile.options).toHaveText([ + await expect(anyProfile.options).toHaveText([ "Buttrfly", - "Collectz", "Hey", - "LensFrens", - "Lensta", + "orb", + "Orna", "Riff", "Soclly", "Tape", @@ -23,90 +22,28 @@ test.describe("Given a Profile link", async () => { }); }); -test.describe("Given a v1 Profile link posted on a social media website/app", async () => { - test.describe("When checking Open Graph meta tags", async () => { - test("Then it should render the expected base-line meta tags", async ({ v1Profile }) => { - await v1Profile.open(); - - expect(await v1Profile.extractOpenGraphProperties()).toMatchObject({ - "og:title": `lens/${v1Profile.handle} profile`, - "og:description": "The Social Layer for Web3 🌿", - "og:url": expect.stringContaining(`/u/lens/${v1Profile.handle}`), - "og:site_name": "Lens Share", - "og:type": "profile", - }); - }); - - test("Then it should include the expected Twitter Card meta tags", async ({ v1Profile }) => { - await v1Profile.open(); - - expect(await v1Profile.extractTwitterMetaTags()).toEqual({ - "twitter:card": "summary_large_image", - "twitter:site": "LensProtocol", - "twitter:title": `lens/${v1Profile.handle} profile`, - "twitter:description": "The Social Layer for Web3 🌿", - "twitter:image": expect.any(String), - "twitter:image:type": "image/png", - }); - }); - }); -}); - -test.describe("Given a v1 Profile link with suffix posted on a social media website/app", async () => { - test.describe("When checking Open Graph meta tags", async () => { - test("Then it should render the expected base-line meta tags", async ({ - v1ProfileWithSuffix, - }) => { - await v1ProfileWithSuffix.open(); - - expect(await v1ProfileWithSuffix.extractOpenGraphProperties()).toMatchObject({ - "og:title": `lens/lensprotocol profile`, - "og:description": "The Social Layer for Web3 🌿", - "og:url": expect.stringContaining(`/u/lens/lensprotocol`), - "og:site_name": "Lens Share", - "og:type": "profile", - }); - }); - - test("Then it should include the expected Twitter Card meta tags", async ({ - v1ProfileWithSuffix, - }) => { - await v1ProfileWithSuffix.open(); - - expect(await v1ProfileWithSuffix.extractTwitterMetaTags()).toEqual({ - "twitter:card": "summary_large_image", - "twitter:site": "LensProtocol", - "twitter:title": `lens/lensprotocol profile`, - "twitter:description": "The Social Layer for Web3 🌿", - "twitter:image": expect.any(String), - "twitter:image:type": "image/png", - }); - }); - }); -}); - -test.describe("Given a v2 Profile link posted on a social media website/app", async () => { +test.describe("Given a Profile link posted on a social media website/app", async () => { test.describe("When checking Open Graph meta tags", async () => { - test("Then it should render the expected base-line meta tags", async ({ v2Profile }) => { - await v2Profile.open(); + test("Then it should render the expected base-line meta tags", async ({ lensProfile }) => { + await lensProfile.open(); - expect(await v2Profile.extractOpenGraphProperties()).toMatchObject({ - "og:title": `${v2Profile.handle} profile`, - "og:description": "The Social Layer for Web3 🌿", - "og:url": expect.stringContaining(`/u/${v2Profile.handle}`), + expect(await lensProfile.extractOpenGraphProperties()).toMatchObject({ + "og:title": `${lensProfile.handle} profile`, + "og:description": "onchain social", + "og:url": expect.stringContaining(`/u/${lensProfile.handle}`), "og:site_name": "Lens Share", "og:type": "profile", }); }); - test("Then it should include the expected Twitter Card meta tags", async ({ v2Profile }) => { - await v2Profile.open(); + test("Then it should include the expected Twitter Card meta tags", async ({ lensProfile }) => { + await lensProfile.open(); - expect(await v2Profile.extractTwitterMetaTags()).toEqual({ + expect(await lensProfile.extractTwitterMetaTags()).toEqual({ "twitter:card": "summary_large_image", "twitter:site": "LensProtocol", - "twitter:title": `${v2Profile.handle} profile`, - "twitter:description": "The Social Layer for Web3 🌿", + "twitter:title": `${lensProfile.handle} profile`, + "twitter:description": "onchain social", "twitter:image": expect.any(String), "twitter:image:type": "image/png", }); @@ -117,26 +54,22 @@ test.describe("Given a v2 Profile link posted on a social media website/app", as test.describe("Given a Profile link posted on a social media website/app", async () => { test.describe("When the link includes the `by` attribution param", async () => { test("Then it should mention the originating app in page `title` and Open Graph `site_name` tag", async ({ - v1Profile, + anyProfile, }) => { - await v1Profile.openAsSharedBy("Hey"); + await anyProfile.openAsSharedBy("Hey"); - expect(await v1Profile.getTitle()).toContain("Hey"); - expect(await v1Profile.extractOpenGraphProperties()).toMatchObject({ + expect(await anyProfile.getTitle()).toContain("Hey"); + expect(await anyProfile.extractOpenGraphProperties()).toMatchObject({ "og:site_name": "Hey", }); }); test("Then it should mention the originating app in Twitter Card `site` if a Twitter handle is provided in the app manifest", async ({ - v1Profile, + anyProfile, }) => { - await v1Profile.openAsSharedBy("Hey"); + await anyProfile.openAsSharedBy("Hey"); - expect(await v1Profile.getTitle()).toContain("Hey"); - expect(await v1Profile.extractOpenGraphProperties()).toMatchObject({ - "og:site_name": "Hey", - }); - expect(await v1Profile.extractTwitterMetaTags()).toMatchObject({ + expect(await anyProfile.extractTwitterMetaTags()).toMatchObject({ "twitter:site": "heydotxyz", }); }); @@ -145,15 +78,14 @@ test.describe("Given a Profile link posted on a social media website/app", async test.describe("Given a Profile link with `by` attribution param", async () => { test.describe("When opening it", async () => { - test("Then it should show the specified app first", async ({ v1Profile }) => { - await v1Profile.openAsSharedBy("Hey"); + test("Then it should show the specified app first", async ({ anyProfile }) => { + await anyProfile.openAsSharedBy("Hey"); - await expect(v1Profile.options).toHaveText([ + await expect(anyProfile.options).toHaveText([ "Hey", "Buttrfly", - "Collectz", - "LensFrens", - "Lensta", + "orb", + "Orna", "Riff", "Soclly", "Tape", @@ -163,55 +95,33 @@ test.describe("Given a Profile link with `by` attribution param", async () => { test.describe("When opening it on a platform not supported by the specified app", async () => { test("Then it should show a message an attribution message before offering other options", async ({ - v1Profile, + anyProfile, }) => { - await v1Profile.openAsSharedBy("orb"); - - await expect(v1Profile.context).toHaveText("Shared via Orb mobile app."); - }); - }); -}); - -test.describe("Given an opened v1 Profile link", async () => { - test.describe("When submitting an app choice", async () => { - test("Then it should open the publication with the selected app", async ({ v1Profile }) => { - await v1Profile.open(); - const url = await v1Profile.justOnce("Hey"); - - expect(url).toMatch(`https://hey.xyz/u/lens/${v1Profile.handle}`); - }); - }); - - test.describe("When submitting an app choice with 'Remember' checkbox selected", async () => { - test("Then it should use the same app for all future publications", async ({ v1Profile }) => { - await v1Profile.open(); - await v1Profile.remember("Hey"); - - const response = await v1Profile.open(); + await anyProfile.openAsSharedBy("phaver"); - expect(response?.url()).toMatch(`https://hey.xyz/u/lens/${v1Profile.handle}`); + await expect(anyProfile.context).toHaveText("Shared via Phaver."); }); }); }); test.describe("Given an opened v2 Profile link", async () => { test.describe("When submitting an app choice", async () => { - test("Then it should open the publication with the selected app", async ({ v2Profile }) => { - await v2Profile.open(); - const url = await v2Profile.justOnce("Hey"); + test("Then it should open the publication with the selected app", async ({ anyProfile }) => { + await anyProfile.open(); + const url = await anyProfile.justOnce("Hey"); - expect(url).toMatch(`https://hey.xyz/u/${v2Profile.handle}`); + expect(url).toMatch(`https://hey.xyz/u/${anyProfile.handle}`); }); }); test.describe("When submitting an app choice with 'Remember' checkbox selected", async () => { - test("Then it should use the same app for all future publications", async ({ v2Profile }) => { - await v2Profile.open(); - await v2Profile.remember("Hey"); + test("Then it should use the same app for all future publications", async ({ anyProfile }) => { + await anyProfile.open(); + await anyProfile.remember("Hey"); - const response = await v2Profile.open(); + const response = await anyProfile.open(); - expect(response?.url()).toMatch(`https://hey.xyz/u/${v2Profile.handle}`); + expect(response?.url()).toMatch(`https://hey.xyz/u/${anyProfile.handle}`); }); }); }); diff --git a/e2e/publications.spec.ts b/e2e/publications.spec.ts index f5a59ed..4611c30 100644 --- a/e2e/publications.spec.ts +++ b/e2e/publications.spec.ts @@ -9,13 +9,7 @@ test.describe("Given a Publication link", async () => { test("Then it should show relevant app options", async ({ imagePost }) => { await imagePost.open(); - await expect(imagePost.options).toHaveText([ - "Buttrfly", - "Collectz", - "Hey", - "Lensta", - "Soclly", - ]); + await expect(imagePost.options).toHaveText(["Buttrfly", "Hey", "orb", "Orna", "Soclly"]); }); }); }); @@ -117,7 +111,14 @@ test.describe("Given a Video Publication link", async () => { }) => { await videoPost.open(); - await expect(videoPost.options).toHaveText(["Buttrfly", "Collectz", "Hey", "Soclly", "Tape"]); + await expect(videoPost.options).toHaveText([ + "Buttrfly", + "Hey", + "orb", + "Orna", + "Soclly", + "Tape", + ]); }); }); }); @@ -127,7 +128,14 @@ test.describe("Given a Publication link with `by` attribution param", async () = test("Then it should show the specified app first", async ({ videoPost }) => { await videoPost.openAsSharedBy("tape"); - await expect(videoPost.options).toHaveText(["Tape", "Buttrfly", "Collectz", "Hey", "Soclly"]); + await expect(videoPost.options).toHaveText([ + "Tape", + "Buttrfly", + "Hey", + "orb", + "Orna", + "Soclly", + ]); }); }); @@ -135,9 +143,9 @@ test.describe("Given a Publication link with `by` attribution param", async () = test("Then it should show a message an attribution message before offering other options", async ({ videoPost, }) => { - await videoPost.openAsSharedBy("orb"); + await videoPost.openAsSharedBy("phaver"); - await expect(videoPost.context).toHaveText("Shared via Orb mobile app."); + await expect(videoPost.context).toHaveText("Shared via Phaver."); }); }); }); diff --git a/manifests/buttrfly-web.json b/manifests/buttrfly-web.json deleted file mode 100644 index 854c391..0000000 --- a/manifests/buttrfly-web.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "appId": "buttrfly", - "name": "Buttrfly", - "description": "Web3 Social Explorer", - "platform": "web", - "icon": { - "url": "https://buttrfly.app/buttrfly-icon-rounded.png", - "background": "#111111" - }, - "routes": { - "home": "https://buttrfly.app/", - "profile": { - "url": "https://buttrfly.app/profile/:handle" - }, - "publication": { - "url": "https://buttrfly.app/post/:id", - "supports": ["ARTICLE", "AUDIO", "EMBED", "IMAGE", "LINK", "TEXT_ONLY", "VIDEO"] - } - } -} diff --git a/manifests/buttrfly.json b/manifests/buttrfly.json index b7afdcb..854c391 100644 --- a/manifests/buttrfly.json +++ b/manifests/buttrfly.json @@ -2,7 +2,7 @@ "appId": "buttrfly", "name": "Buttrfly", "description": "Web3 Social Explorer", - "platform": "mobile", + "platform": "web", "icon": { "url": "https://buttrfly.app/buttrfly-icon-rounded.png", "background": "#111111" diff --git a/manifests/collectz.json b/manifests/collectz.json deleted file mode 100644 index 4007c63..0000000 --- a/manifests/collectz.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "appId": "collectz", - "name": "Collectz", - "description": "Your Collects Town Square", - "platform": "web", - "icon": { - "url": "https://bitcloutweb.azureedge.net/public/lens/images/Icon250.png", - "background": "#000" - }, - "routes": { - "home": "https://collectz.xyz/", - "profile": { - "url": "https://collectz.xyz/u/:handle" - }, - "publication": { - "url": "https://collectz.xyz/c/:id", - "supports": ["IMAGE","AUDIO","VIDEO"] - } - }, - "twitter": "nftz_me" -} diff --git a/manifests/lensfrens.json b/manifests/lensfrens.json deleted file mode 100644 index 06c17e7..0000000 --- a/manifests/lensfrens.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "appId": "lensfrens", - "name": "LensFrens", - "description": "Simple curated page for your lens profiles.", - "platform": "web", - "icon": { - "url": "/icons/lensfrens.svg", - "background": "#272E29" - }, - "routes": { - "home": "https://www.lensfrens.xyz/", - "profile": { - "url": "https://www.lensfrens.xyz/:handle" - } - } -} diff --git a/manifests/lensta.json b/manifests/lensta.json deleted file mode 100644 index cc15fdc..0000000 --- a/manifests/lensta.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "appId": "lensta", - "name": "Lensta", - "description": "Explore & engage with visual content on Lens", - "platform": "web", - "icon": { - "url": "https://ipfs.filebase.io/ipfs/QmeT6Ekrb7xxUUZ6steARbujszzz7rN1LUKNfhXdZvp3Ma", - "background": "#000" - }, - "routes": { - "home": "https://app.lensta.xyz/", - "profile": { - "url": "https://app.lensta.xyz/profile/:handle" - }, - "publication": { - "url": "https://app.lensta.xyz/post/:id", - "supports": ["ARTICLE", "IMAGE", "LINK", "TEXT_ONLY"] - } - }, - "twitter": "lenstaxyz" -} \ No newline at end of file diff --git a/manifests/orb.json b/manifests/orb.json index 41b38de..298a9b1 100644 --- a/manifests/orb.json +++ b/manifests/orb.json @@ -1,8 +1,8 @@ { "appId": "orb", - "name": "Orb", - "description": "Decentralized professional social media app with an end-to-end on-chain credibility system; connecting companies, projects, and people; built with Lens protocol on Polygon chain.", - "platform": "mobile", + "name": "orb", + "description": "Everyday app for web3 social. A community-focused social app for people to connect, interact, and transact in their daily lives on web3 built with Lens Protocol.", + "platform": "web", "icon": { "url": "https://orb.ac/assets/orb-logo-white.png", "background": "#181818" diff --git a/manifests/orna.json b/manifests/orna.json new file mode 100644 index 0000000..1216b6b --- /dev/null +++ b/manifests/orna.json @@ -0,0 +1,21 @@ +{ + "appId": "orna.art", + "name": "Orna", + "description": "Lens collects, people and communities", + "platform": "web", + "icon": { + "url": "https://bitcloutweb.azureedge.net/public/lens/images/Icon_round250OrnaFullTiny.png", + "background": "#000" + }, + "routes": { + "home": "https://orna.art/", + "profile": { + "url": "https://orna.art/u/:handle" + }, + "publication": { + "url": "https://orna.art/c/:id", + "supports": ["IMAGE", "AUDIO", "VIDEO", "THREE_D"] + } + }, + "twitter": "nftz_me" +} diff --git a/next.config.js b/next.config.js index 534254d..75c909b 100644 --- a/next.config.js +++ b/next.config.js @@ -53,9 +53,33 @@ const nextConfig = { destination: "/u/:handle?by=tape", permanent: false, }, - // v1 to v2 handle redirect + { + source: "/p/:id", + has: [ + { + type: "query", + key: "by", + value: "collectz", + }, + ], + destination: "/p/:id?by=orna.art", + permanent: false, + }, { source: "/u/:handle", + has: [ + { + type: "query", + key: "by", + value: "collectz", + }, + ], + destination: "/u/:handle?by=orna.art", + permanent: false, + }, + // v1 to v2 handle redirect + { + source: "/u/:handle.lens", destination: "/u/lens/:handle", permanent: false, }, diff --git a/src/app/u/[namespace]/[localname]/page.tsx b/src/app/u/[namespace]/[localname]/page.tsx index e766583..6a612f2 100644 --- a/src/app/u/[namespace]/[localname]/page.tsx +++ b/src/app/u/[namespace]/[localname]/page.tsx @@ -2,7 +2,7 @@ import { ProfileFragment } from "@lens-protocol/client"; import { never } from "@lens-protocol/shared-kernel"; import truncateMarkdown from "markdown-truncate"; import { ResolvingMetadata } from "next"; -import { notFound } from "next/navigation"; +import { notFound, redirect } from "next/navigation"; import { client } from "@/app/client"; import { SearchParams } from "@/app/types"; @@ -11,7 +11,7 @@ import { twitterHandle } from "@/config"; import { AppManifest, findApp, findFavoriteApp, findProfileApps } from "@/data"; import { formatProfileHandle } from "@/formatters"; import { resolvePlatformType } from "@/utils/device"; -import { getFullHandle } from "@/utils/handle"; +import { getFullHandle, hasV1Suffix, removeV1Suffix } from "@/utils/handle"; import { resolveAttribution } from "@/utils/request"; import { openWith } from "./actions"; @@ -27,6 +27,11 @@ export type ProfilePageProps = { export default async function ProfilePage({ params, searchParams }: ProfilePageProps) { const platform = resolvePlatformType(); + + if (hasV1Suffix(params.localname)) { + redirect(`/u/${params.namespace}/${removeV1Suffix(params.localname)}`); + } + const fullHandle = getFullHandle(params.namespace, params.localname); const profile = await client.profile.fetch({ forHandle: fullHandle }); diff --git a/src/components/AppsList.tsx b/src/components/AppsList.tsx index 4812dd9..859cc18 100644 --- a/src/components/AppsList.tsx +++ b/src/components/AppsList.tsx @@ -26,8 +26,8 @@ export function AppsList({ attribution, options }: AppsListProps) { target="_blank" > {attribution.name} - {" "} - mobile app. + + .

)} diff --git a/src/data/AppManifestSchema.ts b/src/data/AppManifestSchema.ts index c2e63ab..154b5f4 100644 --- a/src/data/AppManifestSchema.ts +++ b/src/data/AppManifestSchema.ts @@ -13,7 +13,7 @@ const AppIdSchema: z.Schema = z }) .min(3) .max(16) - .regex(/^[a-z0-9]+$/i) + .regex(/^[a-z0-9.]+$/i) .transform((value) => value as AppId); const ProfileUrlSchema = z.object( diff --git a/src/data/findProfileApps.ts b/src/data/findProfileApps.ts index 71d4742..c4b9772 100644 --- a/src/data/findProfileApps.ts +++ b/src/data/findProfileApps.ts @@ -24,7 +24,6 @@ export async function findProfileApps( .filter((app) => webOnly(app) && supportsProfileRoute(app)) .sort(withPriorityTo(request.priorityTo)); } - return apps .filter(supportsProfileRoute) .sort(byMobilePlatformFirst) diff --git a/src/data/storage.ts b/src/data/storage.ts index 0f00505..82abf48 100644 --- a/src/data/storage.ts +++ b/src/data/storage.ts @@ -34,6 +34,7 @@ async function readAllManifestFiles(): Promise[]> { return success(manifest); } catch (e) { + console.log(e); assertError(e); return failure(e); } diff --git a/src/utils/handle.ts b/src/utils/handle.ts index 452d5ed..61fd170 100644 --- a/src/utils/handle.ts +++ b/src/utils/handle.ts @@ -1,7 +1,11 @@ const V1_SUFFIX = ".lens"; +export function hasV1Suffix(handle: string): boolean { + return handle.endsWith(V1_SUFFIX); +} + export function removeV1Suffix(handle: string): string { - if (handle.endsWith(".lens")) { + if (hasV1Suffix(handle)) { return handle.slice(0, -V1_SUFFIX.length); }