aboutsummaryrefslogtreecommitdiff
path: root/frontend/src/main.ts
blob: 4868365cb25ee74bd1ed0bf81274829ab37aab29 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
import { marked } from 'marked';
import './style.css'

function renderTextWithFacets(text: string, facets: any[]): string {
  if (!facets || facets.length === 0) {
    return text;
  }

  // convert text to UTF-8 bytes because Bluesky counts bytes in UTF-8
  const encoder = new TextEncoder();
  const postBytes = encoder.encode(text);

  // sort facets by start position
  facets.sort((a, b) => a.byteStart - b.byteStart);

  let result = '';
  let lastPos = 0;

  const decoder = new TextDecoder();
  for (const facet of facets) {
    // add text before this facet
    const beforeBytes = postBytes.slice(lastPos, facet.byteStart);
    result += decoder.decode(beforeBytes);

    // add the faceted text as a link
    const facetBytes = postBytes.slice(facet.byteStart, facet.byteEnd);
    const facetText = decoder.decode(facetBytes);

    switch (facet.type) {
      case 'link':
        result += `<a href="${facet.value}" target="_blank">${facetText}</a>`;
        break;
      case 'mention':
        result += `<a href="https://bsky.app/profile/${facet.value}" target="_blank">${facetText}</a>`;
        break;
      case 'tag':
        result += `<a href="https://bsky.app/hashtag/${facet.value}" target="_blank">#${facetText}</a>`;
        break;
      default: // in case Bluesky ever adds other facet types
        result += facetText; // if we don't recognize the type, just add it as plain text
        break;
    }

    lastPos = facet.byteEnd;
  }

  // add remaining text
  const remainingBytes = postBytes.slice(lastPos);
  result += decoder.decode(remainingBytes);

  return result;
}

function renderAuthor(note: any): string {
  const hasDisplayName = note.authorName && note.authorName !== '';
  const hasValidHandle = note.authorHandle !== 'handle.invalid';
  const pronouns = note.pronouns ? `<span class="muted> · ${note.pronouns}</span>` : '';

  if (hasDisplayName && hasValidHandle) {
    return `<div><strong>${note.authorName}</strong>${pronouns}</div><div class="muted">@${note.authorHandle}</div>`;
  } else if (hasDisplayName && !hasValidHandle) {
    return `<div><strong>${note.authorName}</strong>${pronouns}</div><div class="muted">⚠️ InvalidHandle</div>`;
  } else if (!hasDisplayName && hasValidHandle) {
    return `<div><strong>@${note.authorHandle}</strong>${pronouns}</div>`;
  } else {
    return `<div><strong>⚠️ InvalidHandle</strong>${pronouns}</div>`;
  }
}

async function main() {
  const sessionCheck = await fetch('/api/session', {
    credentials: 'include'
  });
  if (!sessionCheck.ok) {
    window.location.href = '/login.html';
    throw new Error('not authenticated');
  }

  const app = document.querySelector('#app');
  if (!app) {
    console.error('App container not found');
    throw new Error('App container not found');
  }

  app.innerHTML = '<div class="spinner"></div>';

  try {
    const response = await fetch('/api/posts', {
      credentials: 'include'
    });
    if (!response.ok) throw new Error(`HTTP ${response.status}`);

    const notes = await response.json();
    app.innerHTML = '';

    for (const note of notes) {
      let parsedText = '';
      if(note.facets && note.facets.length > 0) {
        parsedText = renderTextWithFacets(note.text || '', note.facets || []);
      } else {
        parsedText = note.text;
      }
      const noteText = marked.parseInline(parsedText, { breaks: true });

      const postHTML = `
      <div class="note">
        <img class="note-avatar" src="${note.avatarURL}" alt="avatar">
        <div class="note-content">
          ${renderAuthor(note)}
          <div class="note-text">${noteText}</div>
          <div class="muted">${new Date(note.createdAt).toLocaleString()}</div>
        </div>
      </div>
    `;
      app.innerHTML += postHTML;
    }
  } catch (err) {
    app.innerHTML = `<div style="color: #f88">error loading posts: ${err}</div>`;
  }
}

main();