Merge branch 'master' into production

This commit is contained in:
Martti Malmi 2023-03-12 09:42:31 +02:00
commit c05b17e878
11 changed files with 171 additions and 152 deletions

View File

@ -61,7 +61,7 @@ npm run test
```
</details>
<br/>
[iris.to](https://iris.to) production version is in the [stable](https://github.com/irislib/iris-messenger/tree/stable) branch.
### Tauri (desktop app)

View File

@ -0,0 +1,21 @@
import { Component } from 'preact';
export default class ErrorBoundary extends Component {
state = { error: null };
static getDerivedStateFromError(error) {
return { error: error.message };
}
componentDidCatch(error) {
console.error(error);
this.setState({ error: error.message });
}
render() {
if (this.state.error) {
return <p style="padding: 0 10px">Error: {this.state.error}</p>;
}
return this.props.children;
}
}

View File

@ -14,6 +14,7 @@ import { translate as t } from '../translations/Translation';
import Button from './buttons/Button';
import EventComponent from './events/EventComponent';
import ErrorBoundary from './ErrorBoundary';
const INITIAL_PAGE_SIZE = 20;
@ -122,7 +123,7 @@ class Feed extends Component {
if (this.state.displayCount < this.state.sortedMessages.length) {
if (
this.props.scrollElement.scrollTop + this.props.scrollElement.clientHeight >=
this.props.scrollElement.scrollHeight - 500
this.props.scrollElement.scrollHeight - 1000
) {
this.setState({ displayCount: this.state.displayCount + INITIAL_PAGE_SIZE });
}
@ -176,6 +177,7 @@ class Feed extends Component {
}
subscribe() {
// TODO use LokiJS persistent dynamicviews so the result set is not recalculated all the time
setTimeout(() => {
this.unsub?.();
let first = true;
@ -218,6 +220,9 @@ class Feed extends Component {
sort(a, b) {
let aVal;
let bVal;
if (!a || !b) return 0;
if (a && !b) return -1;
if (!a && b) return 1;
if (this.state.settings.sortBy === 'created_at') {
aVal = a.created_at;
bVal = b.created_at;
@ -238,12 +243,11 @@ class Feed extends Component {
getPostsAndRepliesByUser(pubkey, includeReplies) {
this.unsub?.();
// TODO apply filters
const desc = this.state.settings.sortDirection === 'desc';
const callback = () => {
// throttle?
const events = Events.db
.chain()
.find({ pubkey })
.find({ pubkey, kind: 1 })
.where((e) => {
// TODO apply all filters from state.settings
if (!includeReplies && e.tags.find((t) => t[0] === 'e')) {
@ -257,58 +261,65 @@ class Feed extends Component {
this.updateSortedMessages(events);
};
callback();
this.unsub = PubSub.subscribe([{ kinds: [1, 3, 5, 7, 9735], limit: 100 }], callback, 'global');
this.unsub = PubSub.subscribe([{ kinds: [1, 5, 7], authors: [pubkey] }], callback);
}
getMessagesByEveryone() {
this.unsub?.();
const settings = this.state.settings;
// TODO apply filters
const desc = this.state.settings.sortDirection === 'desc';
const callback = () => {
// throttle?
const events = Events.db
.chain()
.where((e) => {
// TODO apply all filters from state.settings
if (!this.state.settings.replies && e.tags.find((t) => t[0] === 'e')) {
return false;
}
return true;
})
.data()
.sort((a, b) => this.sort(a, b)) // why loki simplesort doesn't work?
.map((e) => e.id);
const dv = Events.db.addDynamicView('everyone');
dv.applyFind({ kind: 1 });
dv.applyWhere((e) => {
if (!settings.replies && e.tags.find((t) => t[0] === 'e')) {
return false;
}
return true;
});
const simpleSortDesc =
settings.sortBy === 'created_at' ? settings.sortDirection === 'desc' : true;
dv.applySimpleSort('created_at', { desc: simpleSortDesc });
if (settings.sortBy !== 'created_at') {
dv.applySort((a, b) => this.sort(a, b));
}
const callback = throttle(() => {
const events = dv.data().map((e) => e.id);
this.updateSortedMessages(events);
};
}, 1000);
callback();
this.unsub = PubSub.subscribe([{ kinds: [1, 3, 5, 7, 9735], limit: 100 }], callback, 'global');
}
getMessagesByFollows() {
this.unsub?.();
const desc = this.state.settings.sortDirection === 'desc';
const callback = () => {
const dv = Events.db.addDynamicView('follows');
dv.applyFind({ kind: 1 });
dv.applyWhere((e) => {
const followDistance = SocialNetwork.followDistanceByUser.get(e.pubkey);
if (!followDistance || followDistance > 1) {
return false;
}
if (!this.state.settings.replies && e.tags.find((t) => t[0] === 'e')) {
return false;
}
return true;
});
const simpleSortDesc =
this.state.settings.sortBy === 'created_at'
? this.state.settings.sortDirection === 'desc'
: true;
dv.applySimpleSort('created_at', { desc: simpleSortDesc });
if (this.state.settings.sortBy !== 'created_at') {
dv.applySort((a, b) => this.sort(a, b));
}
const callback = throttle(() => {
// throttle?
const events = Events.db
.chain()
.where((e) => {
// TODO apply all filters from state.settings
if (!(SocialNetwork.followDistanceByUser.get(e.pubkey) <= 1)) {
return false;
}
if (!this.state.settings.replies && e.tags.find((t) => t[0] === 'e')) {
return false;
}
return true;
})
.data()
.sort((a, b) => this.sort(a, b)) // why loki simplesort doesn't work?
.map((e) => e.id);
const events = dv.data().map((e) => e.id);
this.updateSortedMessages(events);
};
}, 1000);
callback();
this.unsub = PubSub.subscribe([{ kinds: [1, 3, 5, 7, 9735] }], callback);
this.unsub = PubSub.subscribe([{ kinds: [1, 3, 5, 7, 9735] }], callback, 'global');
}
updateParams(prevState) {
@ -319,7 +330,7 @@ class Feed extends Component {
} else {
url.searchParams.delete('display');
}
window.history.replaceState({ ...window.history.state, state: this.state }, '', url);
this.replaceState();
}
if (prevState.settings.replies !== this.state.settings.replies) {
const url = new URL(window.location);
@ -328,7 +339,7 @@ class Feed extends Component {
} else {
url.searchParams.delete('replies');
}
window.history.replaceState({ ...window.history.state, state: this.state }, '', url);
this.replaceState();
}
if (prevState.settings.realtime !== this.state.settings.realtime) {
const url = new URL(window.location);
@ -337,10 +348,18 @@ class Feed extends Component {
} else {
url.searchParams.delete('realtime');
}
window.history.replaceState({ ...window.history.state, state: this.state }, '', url);
this.replaceState();
}
}
replaceState = throttle(
() => {
window.history.replaceState({ ...window.history.state, state: this.state }, '');
},
1000,
{ leading: true, trailing: true },
);
componentDidUpdate(prevProps, prevState) {
if (!prevProps.scrollElement && this.props.scrollElement) {
this.addScrollHandler();
@ -356,7 +375,7 @@ class Feed extends Component {
this.subscribe();
}
this.handleScroll();
window.history.replaceState({ ...window.history.state, state: this.state }, '');
this.replaceState();
if (!this.state.queuedMessages.length && prevState.queuedMessages.length) {
Helpers.animateScrollTop('.main-view');
}
@ -544,9 +563,8 @@ class Feed extends Component {
}[this.props.index];
const renderAs = this.state.settings.display === 'grid' ? 'NoteImage' : null;
const messages = this.state.sortedMessages
.slice(0, displayCount)
.map((id) => (
const messages = this.state.sortedMessages.slice(0, displayCount).map((id) => (
<ErrorBoundary>
<EventComponent
notification={this.props.index === 'notifications'}
key={id}
@ -555,7 +573,8 @@ class Feed extends Component {
renderAs={renderAs}
feedOpenedAt={this.openedAt}
/>
));
</ErrorBoundary>
));
return (
<div className="msg-feed">
<div>

View File

@ -29,7 +29,7 @@ export default class Menu extends Component {
}
menuLinkClicked(e, a) {
if (a.text === 'feeds') {
if (a?.text === 'feeds') {
this.handleFeedClick(e);
}
localState.get('toggleMenu').put(false);

View File

@ -51,8 +51,12 @@ const onClick = (event, noteId) => {
export default function NoteImage(props: { event: Event; fadeIn?: boolean }) {
// get first image url from event content
if (props.event.kind !== 1) {
console.log('not a note', props.event);
return null;
}
const attachments = [];
const urls = props.event.content.match(/(https?:\/\/[^\s]+)/g);
const urls = props.event.content?.match(/(https?:\/\/[^\s]+)/g);
if (urls) {
urls.forEach((url) => {
let parsedUrl;
@ -63,7 +67,7 @@ export default function NoteImage(props: { event: Event; fadeIn?: boolean }) {
return;
}
if (parsedUrl.pathname.toLowerCase().match(/\.(jpg|jpeg|gif|png|webp)$/)) {
attachments.push({ type: 'image', data: `${parsedUrl.href}` });
attachments.push({ type: 'image', data: parsedUrl.href });
}
});
}

View File

@ -23,7 +23,7 @@ const MAX_ZAPS_BY_NOTE = 1000;
const db = new Loki('iris');
const events = db.addCollection('events', {
indices: ['created_at', 'pubkey'],
indices: ['created_at', 'pubkey', 'kind'],
unique: ['id'],
});
@ -105,34 +105,6 @@ const Events = {
}
}
},
deleteRepostedMsgsFromFeeds(event: Event, feeds: SortedLimitedEventSet[]) {
const repostedEventId = this.getRepostedEventId(event);
const repostedEvent = this.db.by('id', repostedEventId);
// checking that someone isn't hiding posts from feeds with backdated reposts of them
if (repostedEvent?.created_at < event.created_at) {
const otherReposts = this.repostsByMessageId.get(repostedEventId);
for (const feed of feeds) {
feed.delete(repostedEventId);
if (otherReposts) {
for (const repostId of otherReposts) {
if (repostId !== event.id) {
feed.delete(repostId);
}
}
}
}
}
},
deleteRepliedMsgsFromFeeds(event: Event, feeds: SortedLimitedEventSet[]) {
const replyingToEventId = this.getNoteReplyingTo(event);
const replyingToEvent = this.db.by('id', replyingToEventId);
// checking that someone isn't hiding posts from feeds with backdated replies to them
if (replyingToEvent?.created_at < event.created_at) {
for (const feed of feeds) {
feed.delete(replyingToEventId);
}
}
},
getRepostedEventId(event: Event) {
let id = event.tags?.find((tag) => tag[0] === 'e' && tag[3] === 'mention')?.[1];
if (id) {
@ -285,9 +257,11 @@ const Events = {
},
insert(event: Event) {
try {
delete event['$loki'];
this.db.insert(event);
} catch (e) {
console.log('failed to insert event', e);
console.log('failed to insert event', e, typeof e);
// suppress error on duplicate insert. lokijs should throw a different error kind?
}
},
handleMetadata(event: Event) {
@ -421,13 +395,13 @@ const Events = {
}
return true;
},
handle(event: Event, force = false, saveToIdb = true) {
handle(event: Event, force = false, saveToIdb = true): boolean {
if (!event) return;
if (!force && !!this.db.by('id', event.id)) {
return;
return false;
}
if (!force && !this.acceptEvent(event)) {
return;
return false;
}
// Accepting metadata so we still get their name. But should we instead save the name on our own list?
// They might spam with 1 MB events and keep changing their name or something.
@ -456,7 +430,7 @@ const Events = {
switch (event.kind) {
case 0:
if (this.handleMetadata(event) === false) {
return;
return false;
}
break;
case 1:
@ -475,7 +449,7 @@ const Events = {
break;
case 3:
if (SocialNetwork.followEventByUser.get(event.pubkey)?.created_at >= event.created_at) {
return;
return false;
}
this.maybeAddNotification(event);
this.handleFollow(event);
@ -516,12 +490,12 @@ const Events = {
// TODO: don't save e.g. old profile & follow events
// TODO since we're only querying relays since lastSeen, we need to store all beforeseen events and correctly query them on demand
// otherwise feed will be incomplete
const followDistance = SocialNetwork.followDistanceByUser.get(event.pubkey);
if (saveToIdb) {
const followDistance = SocialNetwork.followDistanceByUser.get(event.pubkey);
if (followDistance <= 1) {
// save all our own events and events from people we follow
IndexedDB.saveEvent(event as Event & { id: string });
} else if (followDistance <= 4) {
} else if (followDistance <= 4 && [0, 3].includes(event.kind)) {
// save profiles and follow events up to follow distance 4
IndexedDB.saveEvent(event as Event & { id: string });
}
@ -530,12 +504,13 @@ const Events = {
// go through subscriptions and callback if filters match
for (const sub of PubSub.subscriptions.values()) {
if (!sub.filters) {
return;
continue;
}
if (this.matchesOneFilter(event, sub.filters)) {
if (this.matchFilters(event, sub.filters)) {
sub.callback && sub.callback(event);
}
}
return true;
},
handleNextFutureEvent() {
if (this.futureEventIds.size === 0) {
@ -554,7 +529,7 @@ const Events = {
}, (nextEvent.created_at - Date.now() / 1000) * 1000);
},
// if one of the filters matches, return true
matchesOneFilter(event: Event, filters: Filter[]) {
matchFilters(event: Event, filters: Filter[]) {
for (const filter of filters) {
if (this.matchFilter(event, filter)) {
return true;

View File

@ -1,10 +1,10 @@
import Dexie, { Table } from 'dexie';
import { throttle } from 'lodash';
import { Event, Filter, matchFilter } from '../lib/nostr-tools';
import Events from './Events';
import Key from './Key';
import SocialNetwork from './SocialNetwork';
export class MyDexie extends Dexie {
events!: Table<Event & { id: string }>;
@ -21,38 +21,32 @@ const db = new MyDexie();
export default {
db,
subscriptions: new Set<string>(),
saveQueue: [] as Event[],
clear() {
return db.delete();
},
save: throttle((_this) => {
const events = _this.saveQueue;
_this.saveQueue = [];
db.events.bulkAdd(events).catch((e) => {
// lots of "already exists" errors
// console.error('error saving events', e);
});
}, 500),
saveEvent(event: Event & { id: string }) {
db.events
.add(event)
.catch('ConstraintError', () => {
// fails if already exists
})
.catch((e) => {
console.error('error saving event', e);
});
this.saveQueue.push(event);
this.save(this);
},
init() {
const myPub = Key.getPubKey();
let follows: string[];
db.events
.where({ pubkey: myPub })
.each((event) => {
Events.handle(event, false, false);
})
.then(() => {
follows = Array.from(SocialNetwork.followedByUser.get(myPub) || []);
return db.events
.where('pubkey')
.anyOf(follows)
.each((event) => {
Events.handle(event, false, false);
});
})
.then(() => {
// other follow events
// are they loaded in correct order to build the WoT?
return db.events.where({ kind: 3 }).each((event) => {
Events.handle(event, false, false);
});
@ -62,7 +56,8 @@ export default {
return db.events
.orderBy('created_at')
.reverse()
.limit(3000)
.filter((event) => event.kind === 1)
.limit(5000)
.each((event) => {
Events.handle(event, false, false);
});
@ -71,16 +66,16 @@ export default {
// other events to be loaded on demand
},
subscribe(filters: Filter[]) {
const stringifiedFilters = JSON.stringify(filters);
if (this.subscriptions.has(stringifiedFilters)) {
return;
}
this.subscriptions.add(stringifiedFilters);
const filter1 = filters.length === 1 ? filters[0] : undefined;
let query: any = db.events;
if (filter1.ids) {
query = query.where('id').anyOf(filter1.ids);
} else {
const stringifiedFilters = JSON.stringify(filters);
if (this.subscriptions.has(stringifiedFilters)) {
return;
}
this.subscriptions.add(stringifiedFilters);
if (filter1.authors) {
query = query.where('pubkey').anyOf(filter1.authors);
}

View File

@ -1,5 +1,5 @@
import localForage from 'localforage';
import { debounce } from 'lodash';
import { debounce, throttle } from 'lodash';
import { Event } from '../lib/nostr-tools';
@ -7,40 +7,41 @@ import Events from './Events';
import Key from './Key';
import SocialNetwork from './SocialNetwork';
let latestByFollows;
const getLatestByFollows = () => {
if (latestByFollows) {
return latestByFollows;
}
latestByFollows = Events.db.addDynamicView('latest_by_follows', { persist: true });
latestByFollows.applyFind({ kind: 1 });
latestByFollows.applySimpleSort('created_at', { desc: true });
latestByFollows.applyWhere((event: Event) => {
return SocialNetwork.followDistanceByUser.get(event.pubkey) <= 1;
});
return latestByFollows;
};
let latestByEveryone;
const getLatestByEveryone = () => {
if (latestByEveryone) {
return latestByEveryone;
}
latestByEveryone = Events.db.addDynamicView('latest_by_everyone', { persist: true });
latestByEveryone.applyFind({ kind: 1 });
latestByEveryone.applySimpleSort('created_at', { desc: true });
return latestByEveryone;
};
export default {
loaded: false,
saveEvents: debounce(() => {
const latestMsgs = Events.db
.chain()
.simplesort('created_at')
.where((e: Event) => {
if (e.kind !== 1) {
return false;
}
const followDistance = SocialNetwork.followDistanceByUser.get(e.pubkey);
if (followDistance > 1) {
return false;
}
return true;
})
.limit(100)
.data();
const latestMsgsByEveryone = Events.db
.chain()
.simplesort('created_at')
.where((e: Event) => {
if (e.kind !== 1) {
return false;
}
return true;
})
.limit(100)
.data();
saveEvents: throttle(() => {
const latestMsgs = getLatestByFollows().data().slice(0, 50);
const latestMsgsByEveryone = getLatestByEveryone().data().slice(0, 50);
const notifications = Events.notifications.eventIds
.map((eventId: any) => {
return Events.db.by('id', eventId);
})
.slice(0, 100);
.slice(0, 50);
let dms = [];
for (const set of Events.directMessagesByUser.values()) {
set.eventIds.forEach((eventId: any) => {
@ -48,7 +49,7 @@ export default {
});
}
dms = dms.slice(0, 100);
const kvEvents = Array.from(Events.keyValueEvents.values()).slice(0, 100);
const kvEvents = Array.from(Events.keyValueEvents.values()).slice(0, 50);
localForage.setItem('latestMsgs', latestMsgs);
localForage.setItem('latestMsgsByEveryone', latestMsgsByEveryone);
@ -56,6 +57,8 @@ export default {
localForage.setItem('dms', dms);
localForage.setItem('keyValueEvents', kvEvents);
// TODO save own block and flag events
console.log('saved latestMsgs', latestMsgs.length);
console.log('saved latestMsgsByEveryone', latestMsgsByEveryone.length);
}, 5000),
saveProfilesAndFollows: debounce(() => {
@ -88,8 +91,8 @@ export default {
);
*/
localForage.setItem('profileEvents', profileEvents.slice(0, 100));
localForage.setItem('followEvents', followEvents2.slice(0, 100));
localForage.setItem('profileEvents', profileEvents.slice(0, 50));
localForage.setItem('followEvents', followEvents2.slice(0, 50));
}, 5000),
loadEvents: async function () {

View File

@ -11,6 +11,7 @@ import Events from '../nostr/Events';
import Key from '../nostr/Key';
import SocialNetwork from '../nostr/SocialNetwork';
import { translate as t } from '../translations/Translation';
import {route} from "preact-router";
const bech32 = require('bech32-buffer');
const nostrLogin = async (event) => {
@ -94,6 +95,7 @@ class Login extends Component {
setTimeout(() => {
// TODO remove setTimeout
localState.get('loggedIn').put(true);
route('/following');
}, 100);
}

View File

@ -2,6 +2,7 @@ import { debounce } from 'lodash';
import { createRef } from 'preact';
import Component from '../BaseComponent';
import ErrorBoundary from '../components/ErrorBoundary';
import Header from '../components/Header';
let isInitialLoad = true;
@ -30,7 +31,7 @@ abstract class View extends Component {
class={`main-view ${this.class}`}
id={this.id}
>
{this.renderView()}
<ErrorBoundary>{this.renderView()}</ErrorBoundary>
</div>
</>
);

View File

@ -18,7 +18,6 @@ class ChatList extends Component {
sortedChats: [],
};
}
enableDesktopNotifications() {
if (window.Notification) {
Notification.requestPermission(() => {
@ -39,9 +38,9 @@ class ChatList extends Component {
const bEventIds = chats.get(b).eventIds;
const aLatestEvent = aEventIds.length ? Events.db.by('id', aEventIds[0]) : null;
const bLatestEvent = bEventIds.length ? Events.db.by('id', bEventIds[0]) : null;
if (bLatestEvent.created_at > aLatestEvent.created_at) {
if (bLatestEvent?.created_at > aLatestEvent?.created_at) {
return 1;
} else if (bLatestEvent.created_at < aLatestEvent.created_at) {
} else if (bLatestEvent?.created_at < aLatestEvent?.created_at) {
return -1;
}
return 0;