Skip to content

# MATRIX PRESENCE & TYPING — DAARION.city

Version: 1.0.0

## 0. PURPOSE

Додати у Matrix-чат DAARION (сторінка `/city/[slug]`) базові реальні індикатори:

- хто онлайн у кімнаті,
- хто друкує зараз (typing).

Це робиться поверх уже працюючого Matrix Chat Client.

## 1. MATRIX EVENTS

Матриця дає 2 типи відповідних подій (через `/sync`):

### 1.1. Presence events (`m.presence`)

    {
      "type": "m.presence",
      "sender": "@user:daarion.space",
      "content": {
        "presence": "online",        // "online" | "offline" | "unavailable"
        "last_active_ago": 0,
        "currently_active": true,
        "status_msg": "Working..."
      }
    }

### 1.2. Typing events (`m.typing`)

В `rooms.join[roomId].ephemeral.events`:

    {
      "type": "m.typing",
      "content": {
        "user_ids": ["@user1:daarion.space", "@user2:daarion.space"]
      }
    }

## 2. FRONTEND ARCHITECTURE

### Existing Components:

- `lib/matrix-client.ts` — `MatrixRestClient`
- `MatrixChatRoom` — працює з повідомленнями та статусом підключення

### New Additions:

    ┌─────────────────────────────────────────────────────────────────┐
    │                    MatrixChatRoom Component                     │
    │  ┌─────────────────────────────────────────────────────────┐    │
    │  │  Header: "General · 5 online"                           │    │
    │  └─────────────────────────────────────────────────────────┘    │
    │  ┌─────────────────────────────────────────────────────────┐    │
    │  │                    Messages Area                         │    │
    │  │  [message 1]                                            │    │
    │  │  [message 2]                                            │    │
    │  └─────────────────────────────────────────────────────────┘    │
    │  ┌─────────────────────────────────────────────────────────┐    │
    │  │  Typing: "User abc друкує..."                           │    │
    │  │  [Input field]                                          │    │
    │  └─────────────────────────────────────────────────────────┘    │
    └─────────────────────────────────────────────────────────────────┘

## 3. MATRIX CLIENT: SYNC LOOP

### 3.1. Sync Filter

При виклику `/sync` використовуємо filter:

    {
      "presence": {
        "types": ["m.presence"]
      },
      "room": {
        "timeline": {
          "limit": 50
        },
        "state": {
          "lazy_load_members": true
        },
        "ephemeral": {
          "types": ["m.typing", "m.receipt"]
        }
      }
    }

### 3.2. MatrixRestClient Extensions

    interface PresenceEvent {
      type: 'm.presence';
      sender: string;
      content: {
        presence: 'online' | 'offline' | 'unavailable';
        last_active_ago?: number;
        currently_active?: boolean;
        status_msg?: string;
      };
    }

    interface TypingEvent {
      type: 'm.typing';
      content: {
        user_ids: string[];
      };
    }

    class MatrixRestClient {
      // Callbacks
      onPresence?: (event: PresenceEvent) => void;
      onTyping?: (roomId: string, userIds: string[]) => void;

      // Enhanced sync loop
      private async syncLoop(): Promise<void> {
        while (this.isSyncing) {
          const res = await this.sync(this.syncToken);
          this.syncToken = res.next_batch;

          // Process presence events
          if (res.presence?.events) {
            for (const event of res.presence.events) {
              if (event.type === 'm.presence') {
                this.onPresence?.(event);
              }
            }
          }

          // Process typing events
          if (res.rooms?.join && this.roomId) {
            const roomData = res.rooms.join[this.roomId];
            if (roomData?.ephemeral?.events) {
              for (const event of roomData.ephemeral.events) {
                if (event.type === 'm.typing') {
                  this.onTyping?.(this.roomId, event.content.user_ids);
                }
              }
            }
          }
        }
      }

      // Send typing notification
      async sendTyping(roomId: string, typing: boolean, timeout?: number): Promise<void> {
        await fetch(
          `${this.baseUrl}/_matrix/client/v3/rooms/${encodeURIComponent(roomId)}/typing/${encodeURIComponent(this.userId)}`,
          {
            method: 'PUT',
            headers: this.authHeaders(),
            body: JSON.stringify({
              typing,
              timeout: timeout || 30000
            })
          }
        );
      }
    }

## 4. MATRIXCHATROOM INTEGRATION

### 4.1. State

    // Online users in room
    const [onlineUsers, setOnlineUsers] = useState<Map<string, 'online' | 'offline' | 'unavailable'>>(new Map());

    // Users currently typing
    const [typingUsers, setTypingUsers] = useState<Set<string>>(new Set());

### 4.2. Callbacks

    useEffect(() => {
      if (!matrixClient.current) return;

      // Presence handler
      matrixClient.current.onPresence = (event) => {
        if (!event.sender || !event.content?.presence) return;

        setOnlineUsers(prev => {
          const next = new Map(prev);
          next.set(event.sender, event.content.presence);
          return next;
        });
      };

      // Typing handler
      matrixClient.current.onTyping = (roomId, userIds) => {
        if (roomId !== bootstrap?.matrix_room_id) return;

        // Filter out current user
        const others = userIds.filter(id => id !== bootstrap?.matrix_user_id);
        setTypingUsers(new Set(others));
      };

      return () => {
        if (matrixClient.current) {
          matrixClient.current.onPresence = undefined;
          matrixClient.current.onTyping = undefined;
        }
      };
    }, [bootstrap]);

### 4.3. Send Typing Notification

    // When user starts typing
    const handleInputChange = useCallback(() => {
      if (matrixClient.current && bootstrap) {
        matrixClient.current.sendTyping(bootstrap.matrix_room_id, true);
      }
    }, [bootstrap]);

    // When user stops typing (debounced)
    const handleInputBlur = useCallback(() => {
      if (matrixClient.current && bootstrap) {
        matrixClient.current.sendTyping(bootstrap.matrix_room_id, false);
      }
    }, [bootstrap]);

## 5. UI DISPLAY

### 5.1. Header (Room Info)

    <div className="flex items-center gap-2">
      <span className="text-white font-medium">{room.name}</span>
      <span className="text-slate-400">·</span>
      <span className="text-emerald-400 text-sm">
        {onlineCount} online
      </span>
    </div>

Where `onlineCount`:

    const onlineCount = useMemo(() => {
      let count = 0;
      onlineUsers.forEach((status, userId) => {
        if (status === 'online' || status === 'unavailable') {
          // Optionally exclude current user
          if (userId !== bootstrap?.matrix_user_id) {
            count++;
          }
        }
      });
      return count;
    }, [onlineUsers, bootstrap]);

### 5.2. Typing Indicator

    {typingUsers.size > 0 && (
      <div className="px-4 py-1 text-sm text-slate-400 animate-pulse">
        {typingUsers.size === 1
          ? `${formatUserName(Array.from(typingUsers)[0])} друкує...`
          : 'Декілька учасників друкують...'}
      </div>
    )}

Helper function:

    function formatUserName(userId: string): string {
      // @daarion_abc123:daarion.space -> User abc123
      return userId
        .split(':')[0]
        .replace('@daarion_', 'User ')
        .replace('@', '');
    }

## 6. LIMITATIONS / MVP

- ✅ Presence/typing працює тільки в активній кімнаті (`/city/[slug]`)
- ❌ Не кешуємо статуси між сесіями
- ❌ Не показуємо, хто саме онлайн у списку кімнат
- ❌ Не показуємо read receipts / last seen

## 7. API SUMMARY

### Matrix Client-Server API

Method | Endpoint | Description
--- | --- | ---
GET | `/_matrix/client/v3/sync` | Get presence + typing events
PUT | `/_matrix/client/v3/rooms/{roomId}/typing/{userId}` | Send typing notification

## 8. ROADMAP (далі)

Після цієї фази:

1. Room-level activity:

2. агрегація онлайн/активності на рівні `/city` списку.

3. Read receipts / last read marker.

4. PWA/Mobile presence:

5. збереження останнього статусу офлайн,
6. push при нових повідомленнях у кімнатах.

## 9. ACCEPTANCE CRITERIA

- [ ] Sync loop обробляє `m.presence` та `m.typing` події
- [ ] Header показує кількість online користувачів
- [ ] Typing indicator показує хто друкує
- [ ] Користувач може надсилати typing notification
- [ ] При виході з кімнати callbacks очищуються