@yz-social/civildefense.io 4.4.3 → 4.5.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,100 @@
1
+ // TODO:
2
+ // - clarify common lifecycle management, including existing reply to not get re-initialized
3
+ // - In Alert, get rid of subject (just use tag)
4
+ // - key off 'deleted' instead of payload:null, and bring handling here
5
+ // - Bring pub/sub here, customized for SpatialConversation subclass
6
+ // - Agents
7
+ // - Document lifecycle and subclass requirements
8
+
9
+ export class Tagged { // Maintains cached existence within a (possibly instance-specific) container
10
+
11
+ // These next three instance methods are not generally called directly, but are here for extending by subclasses.
12
+ initialize({...properties} = {}) { // Initialization of a new object. (Includes tag.) Must return this, or null to not cache.
13
+ // Application subclass will typically extend this with UI initialization
14
+ Object.assign(this, properties);
15
+ return this;
16
+ }
17
+ update({...properties} = {}) { // Re-initialize an existing object with properties. Must return this, or null to remove item.
18
+ // This version gives error when specified values (if any) do not match existing.
19
+ // Since axona publications are immutable, an update with different values would generally be meaningless.
20
+ for (const key in properties) {
21
+ const existing = this[key];
22
+ const proposed = properties[key];
23
+ if (JSON.stringify(existing) !== JSON.stringify(proposed)) throw new Error(`Cannot update ${key} ${existing} to ${proposed}.`);
24
+ }
25
+ return this;
26
+ }
27
+ destroy() { // Subclasses extend to remove UI.
28
+ // Subclass extensions of ensure may expect this answer falsy, indicating that an item was removed.
29
+ // NOTE: Does not destroy replies, as these may have been published by others.
30
+ return null;
31
+ }
32
+
33
+ static ensureIn(data, container, kind = container.itemKind) { // update() or initialize() item and remember what those answer. (Falsy is deleted).
34
+ const {tag, payload, ...rest} = data;
35
+ let item = container.getItem(tag);
36
+ if (!payload) return item && container.removeItem(tag)?.destroy();
37
+ if (item) item = item.update(data);
38
+ else item = new kind().initialize(data);
39
+
40
+ if (!item) return container.removeItem(tag)?.destroy();
41
+ return container.setItem(tag, item);
42
+ }
43
+ }
44
+
45
+ export class Reply extends Tagged { // An individual reply to a conversation.
46
+ }
47
+
48
+ export class Conversation extends Tagged { // A conversation with replies.
49
+
50
+ // get/set/removeItem for replies. The instance maintains the collection as a list.
51
+ items = []; // List of replies, in increasing time order.
52
+ getItem(tag) { // Find the reply if known, else falsy.
53
+ const { items } = this;
54
+ return items.find(reply => reply.tag === tag);
55
+ }
56
+ setItem(tag, item) { // Adds reply to cache, maintaining order. Ignores tag.
57
+ const { items } = this;
58
+ items.push(item);
59
+ items.sort((a, b) => a.issuedTime - b.issuedTime); // In case they arrive out of order. Typically just a check.
60
+ return item;
61
+ }
62
+ removeItem(tag) { // Remove reply from cache.
63
+ const { items } = this;
64
+ return items.splice(items.findIndex(reply => reply.tag === tag), 1)?.[0];
65
+ }
66
+ get itemKind() { // Answer class of reply items.
67
+ return Reply;
68
+ }
69
+ ensure(data) { // Initialize or update Reply from data.
70
+ return this.constructor.ensureIn(data, this);
71
+ }
72
+
73
+ // get/set/removeItem for conversations. The class maintains the collection as a dictionary.
74
+ // Multiton pattern, with each subclass getting its own dictionary.
75
+ static _conversations = {}; // Maps tag => conversation
76
+ static get conversations() { // Answer conversation dictionary for this specific class.
77
+ if (!Object.hasOwn(this, '_conversations')) this._conversations = {};
78
+ return this._conversations;
79
+ }
80
+ static items() {
81
+ return Object.values(this.conversations);
82
+ }
83
+ static getItem(tag) { // Get conversation if known, else falsy.
84
+ return this.conversations[tag];
85
+ }
86
+ static setItem(tag, item) { // Cache conversation at tag.
87
+ return this.conversations[tag] = item;
88
+ }
89
+ static removeItem(tag) { // Remove conversation from cache.
90
+ let existing = this.getItem(tag);
91
+ delete this._conversations[tag];
92
+ return existing;
93
+ }
94
+ static get itemKind() { // Answer class of Converstion items.
95
+ return this;
96
+ }
97
+ static ensure(data) { // Initialize or update Conversation from data.
98
+ return this.ensureIn(data, this);
99
+ }
100
+ }
@@ -1,7 +1,8 @@
1
1
  const { localStorage } = globalThis; // For linters.
2
2
  import { stripLeadingEmoji, canonicalTag } from './versions.js';
3
3
  import { Int } from './translations.js';
4
- import { updateSubscriptions, Marker, showMessage } from './map.js';
4
+ import { showMessage } from './map.js';
5
+ import { Alert } from './alert.js';
5
6
  import { resetInactivityTimer, clickTip } from './main.js';
6
7
 
7
8
  // We subscribe to the cartesian product of the list of non-overlapping cells and all hashes.
@@ -26,7 +27,7 @@ export const Hashtags = {
26
27
  // were using the same emoji as each other.
27
28
  hashtags: {},
28
29
  canonical2extended: {},
29
- add(label, active = true, updateMarkers = true) { // Ensure label is a hashtag, initialized to active, and if existing, forcing it active.
30
+ add(label, active = true, updateAlerts = true) { // Ensure label is a hashtag, initialized to active, and if existing, forcing it active.
30
31
  // Return our (possibly new) understanding of the extended hashtag.
31
32
  // Note that only startup-population of tags from persistence would ever specify active=false.
32
33
  // Here we accept a canonical or extended label, updating our records keyed by the canonical part,
@@ -44,7 +45,7 @@ export const Hashtags = {
44
45
  this.canonical2extended[canonical] = extended;
45
46
  if (!oursHasEmoji) {
46
47
  this.onchange({resetSubscriptions: false});
47
- if (updateMarkers) Marker.updateMarkers(canonical, extended);
48
+ if (updateAlerts) Alert.updateAlerts(canonical, extended);
48
49
  }
49
50
  return extended;
50
51
  },
@@ -70,7 +71,7 @@ export const Hashtags = {
70
71
  // Unneeded and not necessarilly meaningful if tag has emoji.
71
72
  return `<minidenticon-svg ${slot ? `slot="${slot}"` : ''} username="${tag}"></minidenticon-svg>`;
72
73
  },
73
- formatMarker(tag) { // HTML (possibly text) to represent tag as a marker on map.
74
+ formatAlert(tag) { // HTML (possibly text) to represent tag as a marker on map.
74
75
  return this.firstEmoji(tag) || this.identicon(tag);
75
76
  },
76
77
  formatPubtag(tag) { // HTML (possibly text) to represent tag with defaulted icon.
@@ -82,8 +83,8 @@ export const Hashtags = {
82
83
  if (redisplaySubscribers) this.resetSubscriberDisplay();
83
84
  localStorage.setItem('hashtags', JSON.stringify(this.hashtags));
84
85
  if (resetSubscriptions) {
85
- updateSubscriptions();
86
- Object.values(Marker.markers).forEach(wrapper => this.hashtags[wrapper.hashtag] || wrapper.destroy());
86
+ Alert.updateSubscriptions();
87
+ Alert.items.forEach(wrapper => this.hashtags[wrapper.hashtag] || wrapper.destroy());
87
88
  }
88
89
  },
89
90
  chipset: document.body.querySelector('.watching-hashtags'), // Element containing the user's chips.
@@ -132,7 +133,7 @@ export const Hashtags = {
132
133
  if (chip.selected) showMessage(Int`Turning "${chip.label}" alerts back on in the map.`, 'instructions');
133
134
  else showMessage(Int`Turning off "${chip.label}" alerts in the map. You can delete the topic altogether with the X.`, 'instructions');
134
135
  this.toggleChip(chip);
135
- Marker.closePopup();
136
+ Alert.closePopup();
136
137
  if (chip.selected) this.setPublish(chip.label);
137
138
  this.onchange({redisplaySubscribers: false});
138
139
  });
@@ -141,7 +142,7 @@ export const Hashtags = {
141
142
  `<md-filled-text-field class="newtag" placeholder="➕${Int`add topic`}"></md-filled-text-field>`);
142
143
  clickTip(this.chipset.firstChild, Int`Add a new topic for which the map should show any alerts.`, event => { // Focusing "add topic".
143
144
  event.stopPropagation();
144
- Marker.closePopup();
145
+ Alert.closePopup();
145
146
  showMessage(Int`Type a new topic name to see any alerts on the map with this topic.`, 'instructions');
146
147
  });
147
148
  this.chipset.firstChild.onchange = event => { // Add the new hashtag.
@@ -151,7 +152,7 @@ export const Hashtags = {
151
152
  .replace(/\s+/g, ' ') // Replace multiple spaces with a single space
152
153
  .normalize('NFD'); // Standardize different ways of making accents into decomposed form - but do not remove them.
153
154
  if (!tag) return;
154
- Marker.closePopup();
155
+ Alert.closePopup();
155
156
  tag = this.add(tag); // Might exist, in which case tag might now be extended.
156
157
  this.setPublish(tag);
157
158
  this.onchange({highlightPublish: true});
@@ -5,7 +5,8 @@ import { openDisplay } from './display.js';
5
5
  import { Agent} from './agent.js';
6
6
  import { P2PWebNetwork } from './p2pWebNetwork.js';
7
7
  import { getPointInCell } from './s2.js';
8
- import { Marker, map, getShareableURL, showMessage, updateLocation, updateSubscriptions, recenterMap, share } from './map.js';
8
+ import { Alert, getShareableURL, share } from './alert.js';
9
+ import { map, showMessage, updateLocation, recenterMap } from './map.js';
9
10
  import './service-manager.js'; // Comment this out and kill service-workers for reload-to-get-latest behavior during development.
10
11
 
11
12
  document.getElementById('appVersion').textContent = appVersion;
@@ -123,7 +124,7 @@ export function openAbout(event) {
123
124
  noteNotificationPermission(window.Notification?.permission);
124
125
  }
125
126
  clickTip('#aboutButton', Int`Information about this app, and options to change how you appear to others.`, event => { // open about
126
- Marker.closePopup();
127
+ Alert.closePopup();
127
128
  openAbout(event);
128
129
  });
129
130
  clickTip('#wipe', Int`Wipe from ${osName()} all personal data and source files for this app.`, async event => {
@@ -242,7 +243,7 @@ function initializeGeolocation(subscribe = false) { // Arrange to constantly upd
242
243
  if (!subscribeOneShot) return;
243
244
  subscribeOneShot = false;
244
245
  resetInactivityTimer(false);
245
- updateSubscriptions([]); // This was for a new node, so supply an empty oldSubscriptions.
246
+ Alert.updateSubscriptions([]); // This was for a new node, so supply an empty oldSubscriptions.
246
247
  };
247
248
  if (!geolocation) {
248
249
  showMessage(Int`Geolocation not supported. Using default location.`, 'error', 'fail');