@nyaruka/temba-components 0.134.5 → 0.135.9
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.
- package/.github/workflows/publish.yml +16 -8
- package/CHANGELOG.md +76 -0
- package/dist/temba-components.js +88 -61
- package/dist/temba-components.js.map +1 -1
- package/out-tsc/src/display/Chat.js +40 -4
- package/out-tsc/src/display/Chat.js.map +1 -1
- package/out-tsc/src/display/Thumbnail.js +65 -8
- package/out-tsc/src/display/Thumbnail.js.map +1 -1
- package/out-tsc/src/live/ContactChat.js +1 -1
- package/out-tsc/src/live/ContactChat.js.map +1 -1
- package/out-tsc/src/utils.js +7 -6
- package/out-tsc/src/utils.js.map +1 -1
- package/out-tsc/test/temba-thumbnail.test.js +120 -0
- package/out-tsc/test/temba-thumbnail.test.js.map +1 -0
- package/out-tsc/test/temba-utils-index.test.js +12 -6
- package/out-tsc/test/temba-utils-index.test.js.map +1 -1
- package/package.json +1 -1
- package/screenshots/truth/contacts/chat-for-archived-contact.png +0 -0
- package/screenshots/truth/contacts/chat-for-blocked-contact.png +0 -0
- package/screenshots/truth/contacts/chat-for-stopped-contact.png +0 -0
- package/src/display/Chat.ts +44 -3
- package/src/display/Thumbnail.ts +67 -8
- package/src/live/ContactChat.ts +1 -1
- package/src/utils.ts +6 -6
- package/test/temba-thumbnail.test.ts +150 -0
- package/test/temba-utils-index.test.ts +14 -6
- package/test-assets/contacts/history.json +4 -0
package/src/display/Thumbnail.ts
CHANGED
|
@@ -10,6 +10,7 @@ enum ThumbnailContentType {
|
|
|
10
10
|
AUDIO = 'audio',
|
|
11
11
|
VIDEO = 'video',
|
|
12
12
|
DOCUMENT = 'document',
|
|
13
|
+
LOCATION = 'location',
|
|
13
14
|
OTHER = 'other'
|
|
14
15
|
}
|
|
15
16
|
|
|
@@ -122,6 +123,28 @@ export class Thumbnail extends RapidElement {
|
|
|
122
123
|
@property({ type: String, attribute: true })
|
|
123
124
|
contentType: string;
|
|
124
125
|
|
|
126
|
+
@property({ type: Number, attribute: false })
|
|
127
|
+
latitude: number;
|
|
128
|
+
|
|
129
|
+
@property({ type: Number, attribute: false })
|
|
130
|
+
longitude: number;
|
|
131
|
+
|
|
132
|
+
// cached tile URL for location thumbnails
|
|
133
|
+
@property({ type: String, attribute: false })
|
|
134
|
+
private tileUrl: string = '';
|
|
135
|
+
|
|
136
|
+
// convert lat/lng to tile coordinates for OSM
|
|
137
|
+
private latLngToTile(lat: number, lng: number, zoom: number) {
|
|
138
|
+
const n = Math.pow(2, zoom);
|
|
139
|
+
const x = Math.floor(((lng + 180) / 360) * n);
|
|
140
|
+
const latRad = (lat * Math.PI) / 180;
|
|
141
|
+
const y = Math.floor(
|
|
142
|
+
((1 - Math.log(Math.tan(latRad) + 1 / Math.cos(latRad)) / Math.PI) / 2) *
|
|
143
|
+
n
|
|
144
|
+
);
|
|
145
|
+
return { x, y, z: zoom };
|
|
146
|
+
}
|
|
147
|
+
|
|
125
148
|
protected updated(
|
|
126
149
|
changes: PropertyValueMap<any> | Map<PropertyKey, unknown>
|
|
127
150
|
): void {
|
|
@@ -162,12 +185,36 @@ export class Thumbnail extends RapidElement {
|
|
|
162
185
|
this.contentType = ThumbnailContentType.VIDEO;
|
|
163
186
|
} else if (contentType.startsWith('application')) {
|
|
164
187
|
this.contentType = ThumbnailContentType.DOCUMENT;
|
|
188
|
+
} else if (contentType.startsWith('geo')) {
|
|
189
|
+
this.contentType = ThumbnailContentType.LOCATION;
|
|
190
|
+
// Parse coordinates from URL which is already stripped of "geo:" prefix
|
|
191
|
+
// Format is now just: lat,lng
|
|
192
|
+
const coords = this.url.match(/^([^,]+),([^,]+)/);
|
|
193
|
+
if (coords) {
|
|
194
|
+
this.latitude = parseFloat(coords[1]);
|
|
195
|
+
this.longitude = parseFloat(coords[2]);
|
|
196
|
+
}
|
|
165
197
|
} else {
|
|
166
198
|
this.contentType = ThumbnailContentType.OTHER;
|
|
167
199
|
}
|
|
168
200
|
}
|
|
169
201
|
}
|
|
170
202
|
}
|
|
203
|
+
|
|
204
|
+
// calculate tile URL when latitude/longitude changes
|
|
205
|
+
if (changes.has('latitude') || changes.has('longitude')) {
|
|
206
|
+
if (
|
|
207
|
+
this.latitude !== undefined &&
|
|
208
|
+
this.longitude !== undefined &&
|
|
209
|
+
!isNaN(this.latitude) &&
|
|
210
|
+
!isNaN(this.longitude)
|
|
211
|
+
) {
|
|
212
|
+
const tile = this.latLngToTile(this.latitude, this.longitude, 13);
|
|
213
|
+
this.tileUrl = `https://tile.openstreetmap.org/${tile.z}/${tile.x}/${tile.y}.png`;
|
|
214
|
+
} else {
|
|
215
|
+
this.tileUrl = '';
|
|
216
|
+
}
|
|
217
|
+
}
|
|
171
218
|
}
|
|
172
219
|
|
|
173
220
|
public handleThumbnailClicked() {
|
|
@@ -176,6 +223,10 @@ export class Thumbnail extends RapidElement {
|
|
|
176
223
|
const lightbox = document.querySelector('temba-lightbox') as Lightbox;
|
|
177
224
|
lightbox.showElement(this);
|
|
178
225
|
}, 100);
|
|
226
|
+
} else if (this.contentType === ThumbnailContentType.LOCATION) {
|
|
227
|
+
// open location in openstreetmap
|
|
228
|
+
const osmUrl = `https://www.openstreetmap.org/?mlat=${this.latitude}&mlon=${this.longitude}#map=15/${this.latitude}/${this.longitude}`;
|
|
229
|
+
window.open(osmUrl, '_blank');
|
|
179
230
|
} else {
|
|
180
231
|
window.open(this.url, '_blank');
|
|
181
232
|
}
|
|
@@ -203,14 +254,22 @@ export class Thumbnail extends RapidElement {
|
|
|
203
254
|
class="observe thumb ${this.contentType}"
|
|
204
255
|
src="${this.url}"
|
|
205
256
|
></img></div>`
|
|
206
|
-
: html
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
257
|
+
: html`
|
|
258
|
+
${this.contentType === ThumbnailContentType.LOCATION
|
|
259
|
+
? html`<img
|
|
260
|
+
style="height:125px;margin-bottom:-3px;border-radius:var(--curvature);"
|
|
261
|
+
src="${this.tileUrl}"
|
|
262
|
+
alt="Location preview"
|
|
263
|
+
/>`
|
|
264
|
+
: html`<div
|
|
265
|
+
style="padding:1em; background:rgba(0,0,0,.05);border-radius:var(--curvature);"
|
|
266
|
+
>
|
|
267
|
+
<temba-icon
|
|
268
|
+
size="1.5"
|
|
269
|
+
name="${ThumbnailIcons[this.contentType]}"
|
|
270
|
+
></temba-icon>
|
|
271
|
+
</div>`}
|
|
272
|
+
`}
|
|
214
273
|
</div>
|
|
215
274
|
`;
|
|
216
275
|
}
|
package/src/live/ContactChat.ts
CHANGED
package/src/utils.ts
CHANGED
|
@@ -84,19 +84,19 @@ export const getHeaders = (headers: any = {}) => {
|
|
|
84
84
|
|
|
85
85
|
const fetchHeaders: any = csrf ? { 'X-CSRFToken': csrf } : {};
|
|
86
86
|
|
|
87
|
-
// include the current
|
|
88
|
-
const
|
|
89
|
-
if (
|
|
90
|
-
fetchHeaders['X-Temba-
|
|
87
|
+
// include the current workspace identifier
|
|
88
|
+
const workspaceUUID = (window as any).workspace?.uuid;
|
|
89
|
+
if (workspaceUUID) {
|
|
90
|
+
fetchHeaders['X-Temba-Workspace'] = workspaceUUID;
|
|
91
91
|
}
|
|
92
92
|
|
|
93
93
|
// mark us as ajax
|
|
94
94
|
fetchHeaders['X-Requested-With'] = 'XMLHttpRequest';
|
|
95
95
|
|
|
96
96
|
Object.keys(headers).forEach((key) => {
|
|
97
|
-
// if we are
|
|
97
|
+
// if we are requesting to service, we omit current workspace identifier
|
|
98
98
|
if (key === 'X-Temba-Service-Org') {
|
|
99
|
-
delete fetchHeaders['X-Temba-
|
|
99
|
+
delete fetchHeaders['X-Temba-Workspace'];
|
|
100
100
|
}
|
|
101
101
|
|
|
102
102
|
fetchHeaders[key] = headers[key];
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { assert, expect } from '@open-wc/testing';
|
|
2
|
+
import { Thumbnail } from '../src/display/Thumbnail';
|
|
3
|
+
import { getComponent } from './utils.test';
|
|
4
|
+
|
|
5
|
+
const TAG = 'temba-thumbnail';
|
|
6
|
+
const getThumbnail = async (attrs: any = {}) => {
|
|
7
|
+
return (await getComponent(TAG, attrs)) as Thumbnail;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
describe('temba-thumbnail', () => {
|
|
11
|
+
it('can be created', async () => {
|
|
12
|
+
const thumbnail = await getThumbnail();
|
|
13
|
+
assert.instanceOf(thumbnail, Thumbnail);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it('renders location thumbnail with map tile', async () => {
|
|
17
|
+
// test that a location attachment properly uses latLngToTile in render
|
|
18
|
+
const thumbnail = await getThumbnail({
|
|
19
|
+
attachment: 'geo:40.7128,-74.0060'
|
|
20
|
+
});
|
|
21
|
+
await thumbnail.updateComplete;
|
|
22
|
+
expect(thumbnail.latitude).to.equal(40.7128);
|
|
23
|
+
expect(thumbnail.longitude).to.equal(-74.006);
|
|
24
|
+
expect(thumbnail.contentType).to.equal('location');
|
|
25
|
+
|
|
26
|
+
// verify the rendered image contains the correct tile URL
|
|
27
|
+
const img = thumbnail.shadowRoot.querySelector('img');
|
|
28
|
+
expect(img).to.exist;
|
|
29
|
+
expect(img.src).to.include('tile.openstreetmap.org');
|
|
30
|
+
expect(img.src).to.include('/13/'); // zoom level
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
describe('latLngToTile', () => {
|
|
34
|
+
it('converts coordinates at 0,0', async () => {
|
|
35
|
+
const thumbnail = await getThumbnail();
|
|
36
|
+
|
|
37
|
+
// test at zoom level 0
|
|
38
|
+
const tile0 = (thumbnail as any).latLngToTile(0, 0, 0);
|
|
39
|
+
expect(tile0.x).to.equal(0);
|
|
40
|
+
expect(tile0.y).to.equal(0);
|
|
41
|
+
expect(tile0.z).to.equal(0);
|
|
42
|
+
|
|
43
|
+
// test at zoom level 10
|
|
44
|
+
const tile10 = (thumbnail as any).latLngToTile(0, 0, 10);
|
|
45
|
+
expect(tile10.x).to.equal(512);
|
|
46
|
+
expect(tile10.y).to.equal(512);
|
|
47
|
+
expect(tile10.z).to.equal(10);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('converts positive coordinates', async () => {
|
|
51
|
+
const thumbnail = await getThumbnail();
|
|
52
|
+
|
|
53
|
+
// test London coordinates at zoom 13
|
|
54
|
+
const londonTile = (thumbnail as any).latLngToTile(51.5074, -0.1278, 13);
|
|
55
|
+
expect(londonTile.x).to.equal(4093);
|
|
56
|
+
expect(londonTile.y).to.equal(2724);
|
|
57
|
+
expect(londonTile.z).to.equal(13);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('converts negative coordinates', async () => {
|
|
61
|
+
const thumbnail = await getThumbnail();
|
|
62
|
+
|
|
63
|
+
// test Sydney coordinates (negative latitude) at zoom 13
|
|
64
|
+
const sydneyTile = (thumbnail as any).latLngToTile(
|
|
65
|
+
-33.8688,
|
|
66
|
+
151.2093,
|
|
67
|
+
13
|
|
68
|
+
);
|
|
69
|
+
expect(sydneyTile.x).to.equal(7536);
|
|
70
|
+
expect(sydneyTile.y).to.equal(4915);
|
|
71
|
+
expect(sydneyTile.z).to.equal(13);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('converts coordinates near the North Pole', async () => {
|
|
75
|
+
const thumbnail = await getThumbnail();
|
|
76
|
+
|
|
77
|
+
// test near North Pole (85.05 is practical limit for Web Mercator)
|
|
78
|
+
const northPoleTile = (thumbnail as any).latLngToTile(85, 0, 5);
|
|
79
|
+
expect(northPoleTile.x).to.equal(16);
|
|
80
|
+
expect(northPoleTile.y).to.equal(0);
|
|
81
|
+
expect(northPoleTile.z).to.equal(5);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('converts coordinates near the South Pole', async () => {
|
|
85
|
+
const thumbnail = await getThumbnail();
|
|
86
|
+
|
|
87
|
+
// test near South Pole (-85.05 is practical limit for Web Mercator)
|
|
88
|
+
const southPoleTile = (thumbnail as any).latLngToTile(-85, 0, 5);
|
|
89
|
+
expect(southPoleTile.x).to.equal(16);
|
|
90
|
+
expect(southPoleTile.y).to.equal(31);
|
|
91
|
+
expect(southPoleTile.z).to.equal(5);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('converts coordinates near the Date Line (positive)', async () => {
|
|
95
|
+
const thumbnail = await getThumbnail();
|
|
96
|
+
|
|
97
|
+
// test near Date Line (179.9 longitude)
|
|
98
|
+
const dateLineTile = (thumbnail as any).latLngToTile(0, 179.9, 10);
|
|
99
|
+
expect(dateLineTile.x).to.equal(1023);
|
|
100
|
+
expect(dateLineTile.y).to.equal(512);
|
|
101
|
+
expect(dateLineTile.z).to.equal(10);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('converts coordinates near the Date Line (negative)', async () => {
|
|
105
|
+
const thumbnail = await getThumbnail();
|
|
106
|
+
|
|
107
|
+
// test near Date Line (-179.9 longitude)
|
|
108
|
+
const dateLineTile = (thumbnail as any).latLngToTile(0, -179.9, 10);
|
|
109
|
+
expect(dateLineTile.x).to.equal(0);
|
|
110
|
+
expect(dateLineTile.y).to.equal(512);
|
|
111
|
+
expect(dateLineTile.z).to.equal(10);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('works correctly with zoom level 1', async () => {
|
|
115
|
+
const thumbnail = await getThumbnail();
|
|
116
|
+
|
|
117
|
+
const tile = (thumbnail as any).latLngToTile(40.7128, -74.006, 1);
|
|
118
|
+
expect(tile.x).to.equal(0);
|
|
119
|
+
expect(tile.y).to.equal(0);
|
|
120
|
+
expect(tile.z).to.equal(1);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('works correctly with zoom level 5', async () => {
|
|
124
|
+
const thumbnail = await getThumbnail();
|
|
125
|
+
|
|
126
|
+
const tile = (thumbnail as any).latLngToTile(40.7128, -74.006, 5);
|
|
127
|
+
expect(tile.x).to.equal(9);
|
|
128
|
+
expect(tile.y).to.equal(12);
|
|
129
|
+
expect(tile.z).to.equal(5);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it('works correctly with zoom level 15', async () => {
|
|
133
|
+
const thumbnail = await getThumbnail();
|
|
134
|
+
|
|
135
|
+
const tile = (thumbnail as any).latLngToTile(40.7128, -74.006, 15);
|
|
136
|
+
expect(tile.x).to.equal(9647);
|
|
137
|
+
expect(tile.y).to.equal(12320);
|
|
138
|
+
expect(tile.z).to.equal(15);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it('works correctly with zoom level 18 (maximum)', async () => {
|
|
142
|
+
const thumbnail = await getThumbnail();
|
|
143
|
+
|
|
144
|
+
const tile = (thumbnail as any).latLngToTile(40.7128, -74.006, 18);
|
|
145
|
+
expect(tile.x).to.equal(77182);
|
|
146
|
+
expect(tile.y).to.equal(98561);
|
|
147
|
+
expect(tile.z).to.equal(18);
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
});
|
|
@@ -189,10 +189,15 @@ describe('utils/index', () => {
|
|
|
189
189
|
expect(headers['X-CSRFToken']).to.equal('form-csrf-token');
|
|
190
190
|
});
|
|
191
191
|
|
|
192
|
-
it('includes org
|
|
193
|
-
(window as any).
|
|
192
|
+
it('includes org uuid when available', () => {
|
|
193
|
+
(window as any).workspace = {
|
|
194
|
+
uuid: '3a7e2f10-b0b3-413d-bd84-626e147d5ed2',
|
|
195
|
+
anon: false
|
|
196
|
+
};
|
|
194
197
|
const headers = getHeaders();
|
|
195
|
-
expect(headers['X-Temba-
|
|
198
|
+
expect(headers['X-Temba-Workspace']).to.equal(
|
|
199
|
+
'3a7e2f10-b0b3-413d-bd84-626e147d5ed2'
|
|
200
|
+
);
|
|
196
201
|
});
|
|
197
202
|
|
|
198
203
|
it('merges provided headers', () => {
|
|
@@ -201,10 +206,13 @@ describe('utils/index', () => {
|
|
|
201
206
|
expect(headers['X-Requested-With']).to.equal('XMLHttpRequest');
|
|
202
207
|
});
|
|
203
208
|
|
|
204
|
-
it('removes X-Temba-
|
|
205
|
-
(window as any).
|
|
209
|
+
it('removes X-Temba-Workspace when X-Temba-Service-Org is provided', () => {
|
|
210
|
+
(window as any).workspace = {
|
|
211
|
+
uuid: '3a7e2f10-b0b3-413d-bd84-626e147d5ed2',
|
|
212
|
+
anon: false
|
|
213
|
+
};
|
|
206
214
|
const headers = getHeaders({ 'X-Temba-Service-Org': 'service-org' });
|
|
207
|
-
expect(headers['X-Temba-
|
|
215
|
+
expect(headers['X-Temba-Workspace']).to.be.undefined;
|
|
208
216
|
expect(headers['X-Temba-Service-Org']).to.equal('service-org');
|
|
209
217
|
});
|
|
210
218
|
});
|
|
@@ -105,6 +105,10 @@
|
|
|
105
105
|
"uuid": "8a81e9e0-10a0-4319-9b00-ce723cfa8303",
|
|
106
106
|
"name": "SMS Channel"
|
|
107
107
|
}
|
|
108
|
+
},
|
|
109
|
+
"_deleted": {
|
|
110
|
+
"created_on": "2025-09-23T20:40:27.239433+00:00",
|
|
111
|
+
"user": { "name": "Rowan Seymour", "uuid": "rowan-user-uuid" }
|
|
108
112
|
}
|
|
109
113
|
},
|
|
110
114
|
{
|