summaryrefslogtreecommitdiff
path: root/ui/lib/store/channels.svelte.js
blob: 86d924edc3aebe00806d03d4d930c088000c7ee9 (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
123
124
125
126
127
128
129
130
131
132
133
import { DateTime } from 'luxon';
import { get } from 'svelte/store'
import { STORE_KEY_CHANNELS_DATA, EPOCH_STRING } from '$lib/constants';

// # Why we don't have a Channel object
//
// For reasons unclear to me, a straight up class definition with a constructor
// doesn't seem to work, reactively. So we resort to this.
//
// Owen suggests that this sentence in the Svelte docs should make the reason
// clear:
//
// > If $state is used with an array or a simple object, the result is a deeply
// > reactive state proxy.
//
// Emphasis on "simple object".
//
// --Kit

export class Channels {
  channels = $state([]);

  constructor({ channelsMetaList }) {
    // This is the state wrapper around the channels meta object. Dammit.
    this.channelsMetaList = channelsMetaList;
  }

  getChannel(channelId) {
    return this.channels.filter((ch) => ch.id === channelId)[0] || null;
  }

  setChannels(channels) {
    // Because this is called at initialization, we need to initialize the matching meta:
    get(this.channelsMetaList).ensureChannels(channels);
    this.channels = channels;
    this.sort();
    return this;
  }

  addChannel(id, name) {
    const newChannel = { id, name }
    this.channels = [...this.channels, newChannel];
    get(this.channelsMetaList).initializeChannel(newChannel);
    this.sort();
    return this;
  }

  deleteChannel(id) {
    const channelIndex = this.channels.map((e) => e.id).indexOf(id);
    if (channelIndex !== -1) {
      this.channels.splice(channelIndex, 1);
    }
    return this;
  }

  sort() {
    this.channels.sort((a, b) => {
      if (a.name < b.name) {
        return -1;
      } else if (a.name > b.name) {
        return 1;
      }
      return 0;
    });
  }
}

export class ChannelsMeta {
  // Store channelId -> { draft = '', lastReadAt = null, scrollPosition = null }
  channelsMeta = $state({});

  constructor({ channelsMetaData }) {
    const channelsMeta = objectMap(channelsMetaData, (ch) => {
      let lastReadAt = ch.lastReadAt;
      if (typeof lastReadAt === 'string') {
        lastReadAt = DateTime.fromISO(lastReadAt);
      }
      if (!Boolean(lastReadAt)) {
        lastReadAt = DateTime.fromISO(EPOCH_STRING);
      }
      return {
        ...ch,
        lastReadAt,
      };
    });
    this.channelsMeta = channelsMeta;
  }

  writeOutToLocalStorage() {
    localStorage.setItem(
      STORE_KEY_CHANNELS_DATA,
      JSON.stringify(this.channelsMeta)
    );
  }

  updateLastReadAt(channelId, at) {
    const channelObject = this.getChannel(channelId);
    // Do it this way, rather than with Math.max tricks, to avoid assignment
    // when we don't need it, to minimize reactive changes:
    if (at > channelObject?.lastReadAt) {
      channelObject.lastReadAt = at;
      this.writeOutToLocalStorage();
    }
  }

  ensureChannels(channelsList) {
    channelsList.forEach(({ id }) => {
      this.initializeChannel(id);
    });
  }

  initializeChannel(channelId) {
    if (!this.channelsMeta[channelId]) {
      const channelData = {
        lastReadAt: null,
        draft: '',
        scrollPosition: null,
      };
      this.channelsMeta[channelId] = channelData;
    }
  }

  getChannel(channelId) {
    return this.channelsMeta[channelId] || null;
  }
}

function objectMap(object, mapFn) {
  return Object.keys(object).reduce((result, key) => {
    result[key] = mapFn(object[key])
    return result
  }, {});
}