diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index bf586111..e62a6fb7 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -10,26 +10,25 @@ on:
jobs:
build:
+ timeout-minutes: 15
runs-on: ubuntu-latest
- strategy:
- matrix:
- node-version: [20]
+ env:
+ TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
+ TURBO_TEAM: ${{ vars.TURBO_TEAM }}
+ TURBO_REMOTE_ONLY: true
steps:
- uses: actions/checkout@v4
- - name: Install pnpm
- uses: pnpm/action-setup@v4
+ - uses: pnpm/action-setup@v4
with:
- version: 9.5.0
-
- - name: Use Node.js ${{ matrix.node-version }}
- uses: actions/setup-node@v4
+ version: 9.6.0
+ - uses: actions/setup-node@v4
with:
- node-version: ${{ matrix.node-version }}
+ node-version: 20
cache: "pnpm"
- name: Install dependencies
- run: pnpm install
+ run: pnpm install --prefer-offline
- name: Run GraphQL diagnostics
run: pnpm gql:check
diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml
new file mode 100644
index 00000000..e77dc86c
--- /dev/null
+++ b/.github/workflows/playwright.yml
@@ -0,0 +1,97 @@
+name: Playwright Tests
+
+on:
+ push:
+ branches:
+ - "**"
+ pull_request:
+ branches:
+ - main
+
+jobs:
+ e2e:
+ if: false # Disable this job temporarily
+ timeout-minutes: 60
+ runs-on: ubuntu-latest
+ strategy:
+ fail-fast: false
+ matrix:
+ shardIndex: [1, 2, 3, 4]
+ shardTotal: [4]
+ env:
+ TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
+ TURBO_TEAM: ${{ vars.TURBO_TEAM }}
+ TURBO_REMOTE_ONLY: true
+
+ steps:
+ - uses: actions/checkout@v4
+ - uses: pnpm/action-setup@v4
+ with:
+ version: 9.6.0
+ - uses: actions/setup-node@v4
+ with:
+ node-version: 20
+ cache: "pnpm"
+
+ - name: Install dependencies
+ run: pnpm install --prefer-offline
+
+ - name: Install Playwright Browsers
+ run: pnpm dlx playwright install --with-deps
+
+ - name: Build project
+ env:
+ TURSO_CONNECTION_URL: ${{ secrets.TURSO_CONNECTION_URL }}
+ SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
+ run: pnpm build
+
+ - name: Run Playwright tests
+ env:
+ TURSO_CONNECTION_URL: ${{ secrets.TURSO_CONNECTION_URL }}
+ TURSO_AUTH_TOKEN: ${{ secrets.TURSO_AUTH_TOKEN }}
+ AES_KEY: ${{ secrets.AES_KEY }}
+ run: pnpm test -- -- --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }}
+
+ - name: Upload blob report to GitHub Actions Artifacts
+ if: ${{ !cancelled() }}
+ uses: actions/upload-artifact@v4
+ with:
+ name: blob-report-${{ matrix.shardIndex }}
+ path: blob-report
+ retention-days: 1
+
+ merge-reports:
+ # if: ${{ !cancelled() }}
+ if: false # Disable this job temporarily
+ needs: [e2e]
+
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ - uses: pnpm/action-setup@v4
+ with:
+ version: 9.5.0
+ - uses: actions/setup-node@v4
+ with:
+ node-version: 20
+ cache: "pnpm"
+
+ - name: Install dependencies
+ run: pnpm install --prefer-offline
+
+ - name: Download blob reports from GitHub Actions Artifacts
+ uses: actions/download-artifact@v4
+ with:
+ path: all-blob-reports
+ pattern: blob-report-*
+ merge-multiple: true
+
+ - name: Merge into HTML Report
+ run: npx playwright merge-reports --reporter html ./all-blob-reports
+
+ - name: Upload HTML report
+ uses: actions/upload-artifact@v4
+ with:
+ name: html-report--attempt-${{ github.run_attempt }}
+ path: playwright-report
+ retention-days: 14
diff --git a/README.md b/README.md
index 62acf3bb..3013b770 100644
--- a/README.md
+++ b/README.md
@@ -26,6 +26,7 @@ If you like this project, please consider giving it a star! ✨ If you wish to s
| `@umamin/db` | Database schema & migrations using Drizzle ORM |
| `@umamin/gql` | GraphQL schema models and resolvers using Pothos |
| `@umamin/aes` | Encryption algorithm using AES in Galois/Counter Mode (AES-GCM) |
+| `@umamin/e2e` | End-to-end testing suite using Playwright |
### Prerequisites
- [`Turso CLI`](https://docs.turso.tech/cli/installation) (for local libSQL server)
diff --git a/apps/www/package.json b/apps/www/package.json
index 19cb4053..5b5d1fb4 100644
--- a/apps/www/package.json
+++ b/apps/www/package.json
@@ -15,35 +15,35 @@
},
"dependencies": {
"@fingerprintjs/botd": "^1.9.1",
- "@graphql-yoga/plugin-csrf-prevention": "^3.6.0",
- "@graphql-yoga/plugin-disable-introspection": "^2.6.0",
- "@graphql-yoga/plugin-persisted-operations": "^3.6.0",
- "@graphql-yoga/plugin-response-cache": "^3.8.0",
- "@hookform/resolvers": "^3.3.4",
+ "@graphql-yoga/plugin-csrf-prevention": "^3.6.2",
+ "@graphql-yoga/plugin-disable-introspection": "^2.6.2",
+ "@graphql-yoga/plugin-persisted-operations": "^3.6.2",
+ "@graphql-yoga/plugin-response-cache": "^3.8.2",
+ "@hookform/resolvers": "^3.9.0",
"@lucia-auth/adapter-drizzle": "^1.0.7",
"@mdx-js/loader": "^3.0.1",
"@mdx-js/react": "^3.0.1",
"@next/mdx": "^14.2.5",
"@node-rs/argon2": "^1.8.3",
- "@sentry/nextjs": "^8",
+ "@sentry/nextjs": "^8.21.0",
"@types/mdx": "^2.0.13",
"@umamin/aes": "workspace:*",
"@umamin/db": "workspace:*",
"@umamin/gql": "workspace:*",
"@umamin/ui": "workspace:*",
- "@urql/core": "^5.0.4",
+ "@urql/core": "^5.0.5",
"@urql/exchange-graphcache": "^7.1.1",
"@urql/exchange-persisted": "^4.3.0",
"@urql/next": "^1.1.1",
- "arctic": "^1.9.1",
+ "arctic": "^1.9.2",
"class-variance-authority": "^0.7.0",
- "clsx": "^2.1.0",
+ "clsx": "^2.1.1",
"date-fns": "^3.6.0",
- "firebase": "^10.12.1",
- "geist": "^1.3.0",
- "gql.tada": "^1.8.2",
+ "firebase": "^10.12.4",
+ "geist": "^1.3.1",
+ "gql.tada": "^1.8.5",
"graphql": "^16.9.0",
- "graphql-yoga": "^5.6.0",
+ "graphql-yoga": "^5.6.2",
"lucia": "^3.2.0",
"lucide-react": "^0.407.0",
"modern-screenshot": "^4.4.39",
@@ -54,18 +54,19 @@
"oslo": "^1.2.1",
"react": "^18.3.1",
"react-dom": "^18.3.1",
- "react-hook-form": "^7.51.2",
+ "react-hook-form": "^7.52.1",
"react-intersection-observer": "^9.10.2",
"remark-gfm": "^4.0.0",
"sonner": "^1.5.0",
- "tailwind-merge": "^2.2.2",
+ "tailwind-merge": "^2.4.0",
"tailwindcss-animate": "^1.0.7",
"urql": "^4.1.0",
"zod": "^3.22.4",
- "zustand": "^4.5.4"
+ "zustand": "^4.5.4",
+ "@whatwg-node/server": "^0.9.46"
},
"devDependencies": {
- "@0no-co/graphqlsp": "^1.12.11",
+ "@0no-co/graphqlsp": "^1.12.12",
"@types/node": "^20",
"@types/react": "^18.3.2",
"@types/react-dom": "^18.3.0",
@@ -74,8 +75,8 @@
"autoprefixer": "^10.0.1",
"eslint": "^8",
"eslint-config-next": "14.2.5",
- "postcss": "^8",
- "tailwindcss": "^3.3.0",
- "typescript": "^5.4.5"
+ "postcss": "^8.4.40",
+ "tailwindcss": "^3.4.7",
+ "typescript": "^5.5.4"
}
}
diff --git a/apps/www/src/app/(profile)/inbox/components/received/card.tsx b/apps/www/src/app/(profile)/inbox/components/received/card.tsx
index 4ce80443..587c0e56 100644
--- a/apps/www/src/app/(profile)/inbox/components/received/card.tsx
+++ b/apps/www/src/app/(profile)/inbox/components/received/card.tsx
@@ -58,7 +58,10 @@ export function ReceivedMessageCard({
-
+
{msg.content}
diff --git a/apps/www/src/app/(profile)/inbox/components/received/list.tsx b/apps/www/src/app/(profile)/inbox/components/received/list.tsx
index c9774a28..67739f31 100644
--- a/apps/www/src/app/(profile)/inbox/components/received/list.tsx
+++ b/apps/www/src/app/(profile)/inbox/components/received/list.tsx
@@ -1,6 +1,7 @@
"use client";
import { toast } from "sonner";
+import dynamic from "next/dynamic";
import { graphql } from "gql.tada";
import { useInView } from "react-intersection-observer";
import { useCallback, useEffect, useState } from "react";
@@ -12,6 +13,10 @@ import { Skeleton } from "@umamin/ui/components/skeleton";
import { useMessageStore } from "@/store/useMessageStore";
import { ReceivedMessageCard, receivedMessageFragment } from "./card";
+const AdContainer = dynamic(() => import("@umamin/ui/ad"), {
+ ssr: false,
+});
+
const MESSAGES_FROM_CURSOR_QUERY = graphql(
`
query ReceivedMessagesFromCursor($input: MessagesFromCursorInput!) {
@@ -96,9 +101,14 @@ export function ReceivedMessagesList({
return (
<>
- {messages?.map((msg) => (
+ {messages?.map((msg, i) => (
+
+ {/* v2-received-list */}
+ {(i + 1) % 5 === 0 && (
+
+ )}
))}
diff --git a/apps/www/src/app/(profile)/inbox/components/received/reply.tsx b/apps/www/src/app/(profile)/inbox/components/received/reply.tsx
index e5e4cc6e..d366d31d 100644
--- a/apps/www/src/app/(profile)/inbox/components/received/reply.tsx
+++ b/apps/www/src/app/(profile)/inbox/components/received/reply.tsx
@@ -115,7 +115,11 @@ export function ReplyDialog(props: Props) {
className="focus-visible:ring-transparent flex-1 text-base resize-none min-h-10 max-h-20"
autoComplete="off"
/>
-
);
}
diff --git a/apps/www/src/app/components/chat-list.tsx b/apps/www/src/app/components/chat-list.tsx
index 8df8a8cb..d2ddc515 100644
--- a/apps/www/src/app/components/chat-list.tsx
+++ b/apps/www/src/app/components/chat-list.tsx
@@ -28,7 +28,10 @@ export const ChatList = ({ imageUrl, question, reply, response }: Props) => {
{reply && (
-
+
{reply}
)}
diff --git a/apps/www/src/app/components/navbar.tsx b/apps/www/src/app/components/navbar.tsx
index b0831507..69165cd8 100644
--- a/apps/www/src/app/components/navbar.tsx
+++ b/apps/www/src/app/components/navbar.tsx
@@ -54,11 +54,12 @@ export async function Navbar() {
)}
-
+
{user ? (
-
+
) : (
-
+
)}
diff --git a/apps/www/src/app/components/progress-dialog.tsx b/apps/www/src/app/components/progress-dialog.tsx
deleted file mode 100644
index 63ec10f0..00000000
--- a/apps/www/src/app/components/progress-dialog.tsx
+++ /dev/null
@@ -1,86 +0,0 @@
-import { toast } from "sonner";
-import dynamic from "next/dynamic";
-import { useEffect, useState } from "react";
-import {
- AlertDialog,
- AlertDialogAction,
- AlertDialogContent,
- AlertDialogDescription,
- AlertDialogFooter,
- AlertDialogHeader,
- AlertDialogTitle,
-} from "@umamin/ui/components/alert-dialog";
-import { Progress } from "@umamin/ui/components/progress";
-
-const AdContainer = dynamic(() => import("@umamin/ui/ad"));
-
-type Props = {
- type: string;
- description: string;
- open: boolean;
- onProgressComplete?: () => void;
- // eslint-disable-next-line no-unused-vars
- onOpenChange: (open: boolean) => void;
-};
-
-export function ProgressDialog({
- type,
- description,
- open,
- onOpenChange,
- onProgressComplete,
-}: Props) {
- const [progress, setProgress] = useState(0);
-
- useEffect(() => {
- if (open) {
- setProgress(0);
-
- const duration = 5000;
- const intervalTime = 100;
- const totalIntervals = duration / intervalTime;
- let currentInterval = 0;
-
- const interval = setInterval(() => {
- currentInterval += 1;
- setProgress(Math.min(100, (currentInterval / totalIntervals) * 100));
-
- if (currentInterval >= totalIntervals) {
- clearInterval(interval);
- toast.success(`${type} sent successfully`);
-
- if (onProgressComplete) {
- onProgressComplete();
- }
- }
- }, intervalTime);
-
- return () => clearInterval(interval);
- }
- }, [open, type]);
-
- return (
-
-
-
-
- {progress === 100 ? `${type} Sent` : `Sending ${type}`}
-
- {description}
-
-
-
-
-
- {progress !== 100 ? (
-
- ) : (
-
- Continue
-
- )}
-
-
-
- );
-}
diff --git a/apps/www/src/app/components/user-card.tsx b/apps/www/src/app/components/user-card.tsx
index f0b30ec7..f3f8ed07 100644
--- a/apps/www/src/app/components/user-card.tsx
+++ b/apps/www/src/app/components/user-card.tsx
@@ -39,7 +39,7 @@ export function UserCard({
{user.displayName ? user.displayName : user.username}
{process.env.NEXT_PUBLIC_VERIFIED_USERS?.split(",").includes(
- user.username,
+ user.username
) &&
}
{user.quietMode && (
@@ -48,7 +48,9 @@ export function UserCard({
- @{user.username}
+
+ @{user.username}
+
diff --git a/apps/www/src/app/notes/components/card.tsx b/apps/www/src/app/notes/components/card.tsx
index d738e1e9..2de4e830 100644
--- a/apps/www/src/app/notes/components/card.tsx
+++ b/apps/www/src/app/notes/components/card.tsx
@@ -3,7 +3,7 @@
import Link from "next/link";
import { useState } from "react";
import { logEvent } from "firebase/analytics";
-import { BadgeCheck, ScanFace } from "lucide-react";
+import { BadgeCheck, MessageCircleOff, ScanFace } from "lucide-react";
import { analytics } from "@/lib/firebase";
import { NotesQueryResult } from "../queries";
@@ -102,9 +102,16 @@ export function NoteCard({ note, menuItems, currentUserId }: Props) {
)}
- {currentUserId && !note.isAnonymous && (
- setOpen(!open)}>
-
+ {currentUserId && note.user?.quietMode && (
+
+ )}
+
+ {currentUserId && !note.isAnonymous && !note.user?.quietMode && (
+ setOpen(!open)}
+ >
+
)}
diff --git a/apps/www/src/app/notes/components/form.tsx b/apps/www/src/app/notes/components/form.tsx
index 797d205f..187f5fc9 100644
--- a/apps/www/src/app/notes/components/form.tsx
+++ b/apps/www/src/app/notes/components/form.tsx
@@ -22,7 +22,6 @@ import { Switch } from "@umamin/ui/components/switch";
import useBotDetection from "@/hooks/use-bot-detection";
import { Textarea } from "@umamin/ui/components/textarea";
import { useDynamicTextarea } from "@/hooks/use-dynamic-textarea";
-import { ProgressDialog } from "@/app/components/progress-dialog";
const UPDATE_NOTE_MUTATION = graphql(`
mutation UpdateNote($content: String!, $isAnonymous: Boolean!) {
@@ -60,7 +59,6 @@ type Props = {
export default function NoteForm({ user, currentNote }: Props) {
useBotDetection();
const [content, setContent] = useState("");
- const [dialogOpen, setDialogOpen] = useState(false);
const [isFetching, setIsFetching] = useState(false);
const [isAnonymous, setIsAnonymous] = useState(false);
const [textAreaCount, setTextAreaCount] = useState(0);
@@ -112,8 +110,8 @@ export default function NoteForm({ user, currentNote }: Props) {
}
if (res.data) {
- setDialogOpen(true);
setContent("");
+ toast.success("Note updated");
updateNote(res.data.updateNote);
}
@@ -127,13 +125,6 @@ export default function NoteForm({ user, currentNote }: Props) {
return (
-
-