Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a4e7e721ac
|
||
|
|
7d497c9afb
|
||
|
|
0d92fc9937
|
||
|
|
7e2bce84db
|
||
|
|
dccad0b47f
|
||
|
|
05160eb5f1
|
||
|
|
fb3b8bef39
|
||
|
|
c870a71e30
|
||
|
|
ddce25f43a
|
||
|
|
4653947454
|
@@ -22,7 +22,7 @@ import iconRounded from '../../icons/icon-rounded.svg?raw';
|
||||
<li>
|
||||
<button type="button" {{on "click" @onSavedPlaces}}>
|
||||
<Icon @name="bookmark" @size={{20}} />
|
||||
<span>Collections</span>
|
||||
<span>Saved Places</span>
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
|
||||
@@ -1005,6 +1005,62 @@ export default class MapComponent extends Component {
|
||||
}
|
||||
});
|
||||
|
||||
decodeVectorTileOsmFeature(feature) {
|
||||
if (!feature?.getId || !feature?.get) {
|
||||
return { decoded: null, reason: 'feature does not expose get()/getId()' };
|
||||
}
|
||||
|
||||
// ol-mapbox-style stores the source-layer name on `mvt:layer`.
|
||||
const sourceLayer = feature.get('mvt:layer') || feature.get('layer');
|
||||
|
||||
// OpenFreeMap uses Planetiler/OpenMapTiles tiles, which encode OSM-backed
|
||||
// feature ids as (osmId * 10) + sourceType.
|
||||
if (sourceLayer !== 'poi') {
|
||||
return {
|
||||
decoded: null,
|
||||
reason: `unsupported source layer: ${sourceLayer || 'unknown'}`,
|
||||
};
|
||||
}
|
||||
|
||||
const rawId = feature.getId();
|
||||
const encodedId = Number(rawId);
|
||||
if (!Number.isSafeInteger(encodedId) || encodedId <= 0) {
|
||||
return {
|
||||
decoded: null,
|
||||
reason: `feature id is not a positive safe integer: ${rawId}`,
|
||||
};
|
||||
}
|
||||
|
||||
const osmType = {
|
||||
1: 'node',
|
||||
2: 'way',
|
||||
3: 'relation',
|
||||
}[encodedId % 10];
|
||||
|
||||
if (!osmType) {
|
||||
return {
|
||||
decoded: null,
|
||||
reason: `feature id suffix ${encodedId % 10} is not an OSM type`,
|
||||
};
|
||||
}
|
||||
|
||||
const osmId = Math.floor(encodedId / 10);
|
||||
if (osmId <= 0) {
|
||||
return {
|
||||
decoded: null,
|
||||
reason: `decoded OSM id is invalid: ${osmId}`,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
decoded: {
|
||||
osmId: String(osmId),
|
||||
osmType,
|
||||
},
|
||||
reason: null,
|
||||
};
|
||||
}
|
||||
|
||||
animateToCrosshair(targetCoords) {
|
||||
if (!this.mapInstance || !this.crosshairElement) return;
|
||||
|
||||
@@ -1141,6 +1197,7 @@ export default class MapComponent extends Component {
|
||||
});
|
||||
let clickedBookmark = null;
|
||||
let clickedSearchResult = null;
|
||||
let clickedTileOsmFeature = null;
|
||||
let selectedFeatureName = null;
|
||||
|
||||
if (features && features.length > 0) {
|
||||
@@ -1155,6 +1212,35 @@ export default class MapComponent extends Component {
|
||||
clickedBookmark = bookmarkFeature.get('originalPlace');
|
||||
} else if (searchResultFeature) {
|
||||
clickedSearchResult = searchResultFeature.get('originalPlace');
|
||||
} else {
|
||||
for (const feature of features) {
|
||||
const sourceLayer =
|
||||
feature.get?.('mvt:layer') || feature.get?.('layer');
|
||||
const featureName = feature.get?.('name');
|
||||
const featureId = feature.getId?.();
|
||||
const { decoded, reason } = this.decodeVectorTileOsmFeature(feature);
|
||||
|
||||
if (decoded) {
|
||||
console.debug(
|
||||
'Decoded vector tile feature to explicit OSM place:',
|
||||
{
|
||||
sourceLayer,
|
||||
featureName,
|
||||
featureId,
|
||||
decoded,
|
||||
}
|
||||
);
|
||||
clickedTileOsmFeature = decoded;
|
||||
break;
|
||||
}
|
||||
|
||||
console.debug('Vector tile feature could not be decoded directly:', {
|
||||
sourceLayer,
|
||||
featureName,
|
||||
featureId,
|
||||
reason,
|
||||
});
|
||||
}
|
||||
}
|
||||
// Also get visual props for standard map click logic later
|
||||
const props = features[0].getProperties();
|
||||
@@ -1179,6 +1265,19 @@ export default class MapComponent extends Component {
|
||||
this.router.transitionTo('place', place);
|
||||
};
|
||||
|
||||
const transitionToExplicitOsmPlace = ({ osmId, osmType }) => {
|
||||
if (
|
||||
this.router.currentRouteName === 'search' ||
|
||||
(this.mapUi.currentSearch && this.mapUi.searchResults.length > 0)
|
||||
) {
|
||||
this.mapUi.returnToSearch = true;
|
||||
}
|
||||
|
||||
this.mapUi.preventNextZoom = true;
|
||||
this.mapUi.showSidebar();
|
||||
this.router.transitionTo('place', `osm:${osmType}:${osmId}`);
|
||||
};
|
||||
|
||||
// Special handling when sidebar is OPEN
|
||||
if (this.args.isSidebarOpen) {
|
||||
// If it's a bookmark or search result, we allow "switching" to it even if sidebar is open
|
||||
@@ -1192,6 +1291,15 @@ export default class MapComponent extends Component {
|
||||
return;
|
||||
}
|
||||
|
||||
if (clickedTileOsmFeature) {
|
||||
console.debug(
|
||||
'Clicked vector tile POI while sidebar open (switching):',
|
||||
clickedTileOsmFeature
|
||||
);
|
||||
transitionToExplicitOsmPlace(clickedTileOsmFeature);
|
||||
return;
|
||||
}
|
||||
|
||||
// Otherwise (empty map or non-bookmark feature), close the sidebar
|
||||
if (this.args.onOutsideClick) {
|
||||
this.args.onOutsideClick();
|
||||
@@ -1212,6 +1320,12 @@ export default class MapComponent extends Component {
|
||||
return;
|
||||
}
|
||||
|
||||
if (clickedTileOsmFeature) {
|
||||
console.debug('Clicked vector tile POI:', clickedTileOsmFeature);
|
||||
transitionToExplicitOsmPlace(clickedTileOsmFeature);
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.mapUi.searchResults && this.mapUi.searchResults.length > 0) {
|
||||
console.debug('Clearing active search and markers on map click');
|
||||
this.router.transitionTo('index');
|
||||
|
||||
@@ -14,6 +14,8 @@ function getPlaceTime(place) {
|
||||
return isNaN(parsed) ? 0 : parsed;
|
||||
}
|
||||
|
||||
const SAVED_LIST_ID = 'saved';
|
||||
|
||||
export default class ListsListController extends Controller {
|
||||
@service router;
|
||||
@service mapUi;
|
||||
@@ -41,6 +43,12 @@ export default class ListsListController extends Controller {
|
||||
}
|
||||
|
||||
get listColor() {
|
||||
if (this.listId === SAVED_LIST_ID) {
|
||||
return getComputedStyle(document.documentElement)
|
||||
.getPropertyValue('--default-list-color')
|
||||
.trim();
|
||||
}
|
||||
|
||||
const list = this.storage.lists.find((l) => l.id === this.listId);
|
||||
if (list && list.color) {
|
||||
return list.color;
|
||||
@@ -51,11 +59,21 @@ export default class ListsListController extends Controller {
|
||||
}
|
||||
|
||||
get listTitle() {
|
||||
if (this.listId === SAVED_LIST_ID) {
|
||||
return 'Saved Places';
|
||||
}
|
||||
|
||||
const list = this.storage.lists.find((l) => l.id === this.listId);
|
||||
return list ? list.title : 'Collections';
|
||||
}
|
||||
|
||||
get places() {
|
||||
if (this.listId === SAVED_LIST_ID) {
|
||||
return [...this.storage.savedPlaces].sort(
|
||||
(a, b) => getPlaceTime(b) - getPlaceTime(a)
|
||||
);
|
||||
}
|
||||
|
||||
const currentList = this.storage.lists.find((l) => l.id === this.listId);
|
||||
const placeRefsIds = new Set(
|
||||
currentList?.placeRefs?.map((ref) => ref.id) || []
|
||||
|
||||
@@ -1,26 +1,22 @@
|
||||
import Route from '@ember/routing/route';
|
||||
import { service } from '@ember/service';
|
||||
|
||||
export default class ListsListRoute extends Route {
|
||||
@service storage;
|
||||
|
||||
model(params) {
|
||||
// Resolve instantly so transition happens in 0ms!
|
||||
return { list_id: params.list_id };
|
||||
}
|
||||
|
||||
setupController(controller, model) {
|
||||
console.debug('DEBUG: setupController controller is:', controller);
|
||||
console.debug(
|
||||
'DEBUG: controller.loadPlacesTask is:',
|
||||
controller?.loadPlacesTask
|
||||
);
|
||||
controller.model = model;
|
||||
super.setupController(controller, model);
|
||||
if (model.list_id === 'saved') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (controller && controller.loadPlacesTask) {
|
||||
controller.loadPlacesTask.perform(model.list_id);
|
||||
} else {
|
||||
console.error('DEBUG: ERROR! controller.loadPlacesTask is undefined!');
|
||||
console.error('controller.loadPlacesTask is undefined');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -184,11 +184,24 @@ export default class ActivityService extends Service {
|
||||
|
||||
const newEntries = groupSocialPhotos(filtered);
|
||||
|
||||
for (const entry of newEntries) {
|
||||
const seenPhotoIds = new Set();
|
||||
for (const entry of this._socialItems) {
|
||||
for (const photo of entry.photos) {
|
||||
seenPhotoIds.add(photo.eventId);
|
||||
}
|
||||
}
|
||||
|
||||
const dedupedEntries = newEntries.filter(
|
||||
(entry) => !entry.photos.every((photo) => seenPhotoIds.has(photo.eventId))
|
||||
);
|
||||
|
||||
if (dedupedEntries.length === 0) return false;
|
||||
|
||||
for (const entry of dedupedEntries) {
|
||||
this._resolveSender(entry);
|
||||
}
|
||||
|
||||
for (const entry of newEntries) {
|
||||
for (const entry of dedupedEntries) {
|
||||
if (entry.osmId) {
|
||||
const bookmarkName = this.placeNameResolver.resolveBookmark(
|
||||
entry.osmId
|
||||
@@ -200,11 +213,11 @@ export default class ActivityService extends Service {
|
||||
}
|
||||
}
|
||||
|
||||
this._socialItems = [...this._socialItems, ...newEntries];
|
||||
this._socialItems = [...this._socialItems, ...dedupedEntries];
|
||||
this._mergeItems();
|
||||
|
||||
void this.placeNameResolver
|
||||
.resolveInBackground(newEntries)
|
||||
.resolveInBackground(dedupedEntries)
|
||||
.then(() => this._mergeItems());
|
||||
|
||||
return true;
|
||||
|
||||
@@ -11,6 +11,12 @@ export default class ListsIndexTemplate extends Component {
|
||||
@service router;
|
||||
@service mapUi;
|
||||
|
||||
savedList = {
|
||||
id: 'saved',
|
||||
title: 'Saved',
|
||||
color: 'var(--default-list-color)',
|
||||
};
|
||||
|
||||
styleFor(color) {
|
||||
const finalColor =
|
||||
color ||
|
||||
@@ -46,7 +52,7 @@ export default class ListsIndexTemplate extends Component {
|
||||
<span class="sidebar-header-icon-wrapper">
|
||||
<Icon @name="bookmark" @size={{20}} @color="#898989" />
|
||||
</span>
|
||||
Collections
|
||||
Saved Places
|
||||
</h2>
|
||||
<button type="button" class="close-btn" {{on "click" this.close}}>
|
||||
<Icon @name="x" @size={{20}} @color="#333" />
|
||||
@@ -55,6 +61,30 @@ export default class ListsIndexTemplate extends Component {
|
||||
|
||||
<div class="sidebar-content">
|
||||
<ul class="places-list">
|
||||
<li>
|
||||
<button
|
||||
type="button"
|
||||
class="lists-index-item"
|
||||
{{on "click" (fn this.selectList this.savedList.id)}}
|
||||
>
|
||||
<div class="lists-index-item-left">
|
||||
{{! template-lint-disable no-inline-styles }}
|
||||
<span
|
||||
class="list-color-dot"
|
||||
style={{this.styleFor this.savedList.color}}
|
||||
></span>
|
||||
<div class="lists-index-name">{{this.savedList.title}}</div>
|
||||
</div>
|
||||
<div class="lists-index-count">
|
||||
{{#if this.storage.savedPlaces.length}}
|
||||
{{this.storage.savedPlaces.length}}
|
||||
places
|
||||
{{else}}
|
||||
empty
|
||||
{{/if}}
|
||||
</div>
|
||||
</button>
|
||||
</li>
|
||||
{{#each this.storage.lists as |list|}}
|
||||
<li>
|
||||
<button
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "marco",
|
||||
"version": "1.33.0",
|
||||
"version": "1.33.1",
|
||||
"private": true,
|
||||
"description": "Unhosted maps app",
|
||||
"repository": {
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1
-1
@@ -42,7 +42,7 @@
|
||||
<meta name="msapplication-TileColor" content="#F6E9A6">
|
||||
<meta name="msapplication-TileImage" content="/icons/icon-144.png">
|
||||
|
||||
<script type="module" crossorigin src="/assets/main-D9MIfkGZ.js"></script>
|
||||
<script type="module" crossorigin src="/assets/main-CdHYOqFD.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/main-CZWBbnnc.css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
@@ -16,8 +16,16 @@ class MockStorageService extends Service {
|
||||
id: 'place-123',
|
||||
title: 'Mountain Trail',
|
||||
geohash: 'u33dc0',
|
||||
createdAt: '2023-01-02T12:00:00.000Z',
|
||||
osmTags: { name: 'Mountain Trail' },
|
||||
},
|
||||
{
|
||||
id: 'place-456',
|
||||
title: 'Beach View',
|
||||
geohash: 'u33dc1',
|
||||
createdAt: '2023-01-03T12:00:00.000Z',
|
||||
osmTags: { name: 'Beach View' },
|
||||
},
|
||||
];
|
||||
lists = [
|
||||
{
|
||||
@@ -74,27 +82,56 @@ module('Acceptance | collections navigation', function (hooks) {
|
||||
assert.dom('.sidebar.app-menu-pane').exists('App menu sidebar is open');
|
||||
assert
|
||||
.dom('.app-menu')
|
||||
.includesText('Collections', 'Menu contains Collections link');
|
||||
.includesText('Saved Places', 'Menu contains Saved Places link');
|
||||
|
||||
// 3. Transition to Collections Index (List of lists)
|
||||
await click(document.querySelectorAll('.app-menu button')[0]); // Click "Collections"
|
||||
// 3. Transition to Saved Places index (list of lists)
|
||||
await click(document.querySelectorAll('.app-menu button')[0]); // Click "Saved Places"
|
||||
assert.strictEqual(currentURL(), '/lists', 'Transitions to /lists index');
|
||||
assert
|
||||
.dom('.sidebar-header-text-centered')
|
||||
.includesText('Collections', 'Header is centered and titled Collections');
|
||||
.includesText(
|
||||
'Saved Places',
|
||||
'Header is centered and titled Saved Places'
|
||||
);
|
||||
assert
|
||||
.dom('.lists-index-item')
|
||||
.exists({ count: 2 }, 'Renders our 2 mocked list items');
|
||||
.exists({ count: 3 }, 'Renders Saved plus our 2 mocked list items');
|
||||
assert
|
||||
.dom(document.querySelectorAll('.lists-index-item')[0])
|
||||
.includesText('Saved', 'Saved appears as the first list item');
|
||||
assert
|
||||
.dom(document.querySelectorAll('.lists-index-item')[0])
|
||||
.includesText('2 places', 'Saved shows the total saved places count');
|
||||
|
||||
// 4. Transition to a specific list (Want to go)
|
||||
await click(document.querySelectorAll('.lists-index-item')[0]); // Click "Want to go"
|
||||
// 4. Transition to the synthetic Saved list
|
||||
await click(document.querySelectorAll('.lists-index-item')[0]);
|
||||
assert.strictEqual(
|
||||
currentURL(),
|
||||
'/lists/saved',
|
||||
'Transitions to /lists/saved'
|
||||
);
|
||||
|
||||
await waitFor('.places-list');
|
||||
assert
|
||||
.dom(document.querySelectorAll('.places-list .place-name')[0])
|
||||
.hasText('Beach View', 'Saved Places shows newest saved place first');
|
||||
assert
|
||||
.dom(document.querySelectorAll('.places-list .place-name')[1])
|
||||
.hasText('Mountain Trail', 'Saved Places includes all saved places');
|
||||
|
||||
// 5. Go back to the Saved Places index
|
||||
await click('.sidebar-header .back-btn');
|
||||
assert.strictEqual(currentURL(), '/lists', 'Goes back to /lists index');
|
||||
|
||||
// 6. Transition to a specific real list (Want to go)
|
||||
await click(document.querySelectorAll('.lists-index-item')[1]); // Click "Want to go"
|
||||
assert.strictEqual(
|
||||
currentURL(),
|
||||
'/lists/to-go',
|
||||
'Transitions instantly to /lists/to-go'
|
||||
);
|
||||
|
||||
// 5. Verify background loading spinner shows up, then results populate
|
||||
// 7. Verify background loading spinner shows up, then results populate
|
||||
await waitFor('.places-list');
|
||||
assert
|
||||
.dom('.places-list .place-name')
|
||||
@@ -103,19 +140,37 @@ module('Acceptance | collections navigation', function (hooks) {
|
||||
.dom('.places-list .place-type')
|
||||
.hasText('Saved place', 'Place type displays Saved place correctly');
|
||||
|
||||
// 6. Click back button in collection list header
|
||||
// 8. Click back button in collection list header
|
||||
await click('.sidebar-header .back-btn');
|
||||
assert.strictEqual(currentURL(), '/lists', 'Goes back to /lists index');
|
||||
|
||||
// 7. Click back button in collections index header
|
||||
// 9. Click back button in collections index header
|
||||
await click('.sidebar-header .back-btn');
|
||||
assert.strictEqual(currentURL(), '/menu', 'Goes back to main menu route');
|
||||
|
||||
// 8. Close sidebar
|
||||
// 10. Close sidebar
|
||||
await click('.sidebar-header .close-btn');
|
||||
assert.strictEqual(currentURL(), '/', 'Sidebar closed and returned home');
|
||||
});
|
||||
|
||||
test('clicking a place inside Saved Places sets returnToRoute and returns gracefully on back click', async function (assert) {
|
||||
await visit('/lists/saved');
|
||||
await waitFor('.places-list');
|
||||
|
||||
await click('.place-item');
|
||||
assert.ok(
|
||||
currentURL().includes('/place/place-456'),
|
||||
'Transitions to the newest saved place details route'
|
||||
);
|
||||
|
||||
await click('.back-btn');
|
||||
assert.strictEqual(
|
||||
currentURL(),
|
||||
'/lists/saved',
|
||||
'Returns gracefully back to the Saved Places view'
|
||||
);
|
||||
});
|
||||
|
||||
test('clicking a place inside a collection sets returnToRoute and returns gracefully on back click', async function (assert) {
|
||||
await visit('/lists/to-go');
|
||||
await waitFor('.places-list');
|
||||
@@ -136,7 +191,7 @@ module('Acceptance | collections navigation', function (hooks) {
|
||||
);
|
||||
});
|
||||
|
||||
test('places inside a collection are sorted by createdAt descending', async function (assert) {
|
||||
test('places inside Saved Places are sorted by createdAt descending', async function (assert) {
|
||||
class SortedMockStorageService extends Service {
|
||||
initialSyncDone = true;
|
||||
savedPlaces = [
|
||||
@@ -203,7 +258,7 @@ module('Acceptance | collections navigation', function (hooks) {
|
||||
this.owner.unregister('service:storage');
|
||||
this.owner.register('service:storage', SortedMockStorageService);
|
||||
|
||||
await visit('/lists/to-go');
|
||||
await visit('/lists/saved');
|
||||
await waitFor('.places-list');
|
||||
|
||||
const placeNames = Array.from(
|
||||
@@ -213,7 +268,7 @@ module('Acceptance | collections navigation', function (hooks) {
|
||||
assert.deepEqual(
|
||||
placeNames,
|
||||
['Newest Place', 'Middle Place', 'Oldest Place'],
|
||||
'Places are ordered by createdAt in descending order'
|
||||
'Saved Places are ordered by createdAt in descending order'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
import MapComponent from 'marco/components/map';
|
||||
import { module, test } from 'qunit';
|
||||
|
||||
module('Unit | Component | map', function () {
|
||||
test('it decodes Planetiler vector tile POI ids into OSM ids and types', function (assert) {
|
||||
const feature = {
|
||||
get(key) {
|
||||
if (key === 'mvt:layer') return 'poi';
|
||||
return undefined;
|
||||
},
|
||||
getId() {
|
||||
return 12342;
|
||||
},
|
||||
};
|
||||
|
||||
const result = MapComponent.prototype.decodeVectorTileOsmFeature(feature);
|
||||
|
||||
assert.deepEqual(result, {
|
||||
decoded: {
|
||||
osmId: '1234',
|
||||
osmType: 'way',
|
||||
},
|
||||
reason: null,
|
||||
});
|
||||
});
|
||||
|
||||
test('it ignores non-POI vector tile features', function (assert) {
|
||||
const feature = {
|
||||
get(key) {
|
||||
if (key === 'mvt:layer') return 'transportation';
|
||||
return undefined;
|
||||
},
|
||||
getId() {
|
||||
return 12342;
|
||||
},
|
||||
};
|
||||
|
||||
assert.deepEqual(
|
||||
MapComponent.prototype.decodeVectorTileOsmFeature(feature),
|
||||
{
|
||||
decoded: null,
|
||||
reason: 'unsupported source layer: transportation',
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
test('it ignores unsupported or invalid vector tile ids', function (assert) {
|
||||
const feature = {
|
||||
get(key) {
|
||||
if (key === 'mvt:layer') return 'poi';
|
||||
return undefined;
|
||||
},
|
||||
getId() {
|
||||
return 12340;
|
||||
},
|
||||
};
|
||||
|
||||
assert.deepEqual(
|
||||
MapComponent.prototype.decodeVectorTileOsmFeature(feature),
|
||||
{
|
||||
decoded: null,
|
||||
reason: 'feature id suffix 0 is not an OSM type',
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -787,6 +787,65 @@ module('Unit | Service | activity', function (hooks) {
|
||||
assert.false(called, 'no fetch when items is empty');
|
||||
});
|
||||
|
||||
test('loadMore does not append duplicate photo group', async function (assert) {
|
||||
const service = this.owner.lookup('service:activity');
|
||||
service._userPubkey = USER_PUBKEY;
|
||||
service._sourceMode = 'explore';
|
||||
service.sourceMode = 'explore';
|
||||
service.nostrData._contactPubkeys = new Set([SENDER_PUBKEY]);
|
||||
|
||||
const STRANGER_PUBKEY = 'c'.repeat(64);
|
||||
service.nostrData.isTrustedEvent = (event) =>
|
||||
event.pubkey === STRANGER_PUBKEY;
|
||||
|
||||
const NOW = Math.floor(Date.now() / 1000);
|
||||
service._since = NOW;
|
||||
|
||||
const photo1 = makePhotoEvent({
|
||||
id: 'd1'.padEnd(64, '0'),
|
||||
author: STRANGER_PUBKEY,
|
||||
placeIdentifier: 'osm:node:100',
|
||||
created_at: NOW - 100,
|
||||
url: 'https://x.com/p1.jpg',
|
||||
});
|
||||
const photo2 = makePhotoEvent({
|
||||
id: 'd2'.padEnd(64, '0'),
|
||||
author: STRANGER_PUBKEY,
|
||||
placeIdentifier: 'osm:node:100',
|
||||
created_at: NOW - 200,
|
||||
url: 'https://x.com/p2.jpg',
|
||||
});
|
||||
const photo3 = makePhotoEvent({
|
||||
id: 'd3'.padEnd(64, '0'),
|
||||
author: STRANGER_PUBKEY,
|
||||
placeIdentifier: 'osm:node:100',
|
||||
created_at: NOW - 300,
|
||||
url: 'https://x.com/p3.jpg',
|
||||
});
|
||||
|
||||
service._updateSocialItems([photo1, photo2, photo3]);
|
||||
assert.strictEqual(
|
||||
service.items.length,
|
||||
1,
|
||||
'one grouped entry with 3 photos'
|
||||
);
|
||||
assert.strictEqual(service.items[0].photos.length, 3);
|
||||
|
||||
service.nostrData.fetchActivityPhotos = async () => ({
|
||||
cacheEvents: [photo1, photo2, photo3],
|
||||
networkEvents: Promise.resolve([]),
|
||||
});
|
||||
|
||||
await service.loadMore();
|
||||
|
||||
assert.strictEqual(service.items.length, 1, 'duplicate group not appended');
|
||||
assert.strictEqual(
|
||||
service.items[0].photos.length,
|
||||
3,
|
||||
'photo count unchanged'
|
||||
);
|
||||
});
|
||||
|
||||
test('stop resets isLoadingMore and isLoading', function (assert) {
|
||||
const service = this.owner.lookup('service:activity');
|
||||
service._isLoadingMore = true;
|
||||
|
||||
Reference in New Issue
Block a user