17 Commits

Author SHA1 Message Date
7cec0f7d61 ssb: Fix private conversation keyboard alt+navigation. #125
Some checks failed
Build Tilde Friends / Build-All (push) Has been cancelled
2025-08-13 19:24:25 -04:00
f902d0374c ssb: Start to break out private messages by conversation. #125 2025-08-13 19:16:34 -04:00
b5f0a0c4f7 ssb: Add support for registering for blob added notifications similarly to messages. I want to use this to load images on the fly. 2025-08-13 18:26:42 -04:00
00623cea09 android: Recompile your app with 16 KB native library alignment. 2025-08-13 18:02:20 -04:00
ed4f1d6f2c android: Be smarter about the file watcher.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 32m12s
2025-08-13 17:51:04 -04:00
73f4a3407f ssb: Allow showing raw messages for contact messages. 2025-08-13 12:14:31 -04:00
6f11318e84 update: speedscope 1.23.1.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 32m56s
2025-08-13 12:06:47 -04:00
e88ee91f0e ssb: More reliably load private messages.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 32m50s
2025-08-06 12:10:51 -04:00
3f8daf257c update: OpenSSL 3.5.2.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 30m58s
2025-08-05 12:16:03 -04:00
dc387acadc ssb: Make progress bar brighter. 2025-08-02 12:16:07 -04:00
68aa41ab96 android: Tweaking random flags until ANRs subside.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 30m34s
2025-08-02 12:09:08 -04:00
85b23437b3 docs: Fix all the TODOCS. #39
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 31m17s
2025-08-02 09:07:45 -04:00
c59fba817d ssb: Show the progress indicator more consistently.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 30m54s
2025-07-31 12:48:45 -04:00
c3415ab75c docs: Expose the rest of core to docs.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 31m24s
2025-07-30 20:25:20 -04:00
f1d0151d71 ssb: Make the progress bar more indefinite-looking.
Some checks failed
Build Tilde Friends / Build-All (push) Has been cancelled
2025-07-30 20:04:34 -04:00
3c5c1756d1 ssb: A progress bar experiment. 2025-07-30 19:49:08 -04:00
6a6b65d1b3 build: Update nix config. Start building 0.2025.8, switching to calver.
Some checks failed
Build Tilde Friends / Build-All (push) Has been cancelled
2025-07-30 19:26:26 -04:00
25 changed files with 479 additions and 194 deletions

View File

@@ -16,9 +16,9 @@ MAKEFLAGS += --no-builtin-rules
## LD := Linker. ## LD := Linker.
## ANDROID_SDK := Path to the Android SDK. ## ANDROID_SDK := Path to the Android SDK.
VERSION_CODE := 40 VERSION_CODE := 41
VERSION_CODE_IOS := 15 VERSION_CODE_IOS := 16
VERSION_NUMBER := 0.0.33 VERSION_NUMBER := 0.2025.8-wip
VERSION_NAME := This program kills fascists. VERSION_NAME := This program kills fascists.
IPHONEOS_VERSION_MIN=14.0 IPHONEOS_VERSION_MIN=14.0
@@ -253,7 +253,10 @@ $(ANDROID_TARGETS): CFLAGS += \
-fno-asynchronous-unwind-tables \ -fno-asynchronous-unwind-tables \
-funwind-tables \ -funwind-tables \
-Wno-unknown-warning-option -Wno-unknown-warning-option
$(ANDROID_TARGETS): LDFLAGS += --sysroot $(ANDROID_NDK)/toolchains/llvm/prebuilt/linux-x86_64/sysroot -fPIC $(ANDROID_TARGETS): LDFLAGS += \
--sysroot $(ANDROID_NDK)/toolchains/llvm/prebuilt/linux-x86_64/sysroot \
-Wl,-z,max-page-size=16384 \
-fPIC
$(DEBUG_TARGETS): CFLAGS += -DDEBUG -Og $(DEBUG_TARGETS): CFLAGS += -DDEBUG -Og
$(DEBUG_TARGETS): LDFLAGS += -Og $(DEBUG_TARGETS): LDFLAGS += -Og
$(RELEASE_TARGETS): CFLAGS += \ $(RELEASE_TARGETS): CFLAGS += \
@@ -1140,6 +1143,11 @@ releaseapkgo: out/TildeFriends-arm-release.apk ## Build, install, and run a rele
@adb shell am start com.unprompted.tildefriends/.TildeFriendsActivity @adb shell am start com.unprompted.tildefriends/.TildeFriendsActivity
.PHONY: releaseapkgo .PHONY: releaseapkgo
x86releaseapkgo: out/TildeFriends-x86-release.apk ## Build, install, and run an x86 release Android APK.
@adb install -r $<
@adb shell am start com.unprompted.tildefriends/.TildeFriendsActivity
.PHONY: x86releaseapkgo
apklog: ## Display Android log output. apklog: ## Display Android log output.
@adb logcat *:S tildefriends @adb logcat *:S tildefriends
.PHONY: apklog .PHONY: apklog

View File

@@ -1,5 +1,5 @@
{ {
"type": "tildefriends-app", "type": "tildefriends-app",
"emoji": "🦀", "emoji": "🦀",
"previous": "&DGtlnm5wWRZCgJMF8JsP6VtzNRrd4KLoERJRpFULqOY=.sha256" "previous": "&5T+xPy3LhgmU2ape4dlJLRhYhmE5J1SQkI+wFm6Fss4=.sha256"
} }

View File

@@ -22,9 +22,11 @@ class TfElement extends LitElement {
guest: {type: Boolean}, guest: {type: Boolean},
url: {type: String}, url: {type: String},
private_messages: {type: Array}, private_messages: {type: Array},
grouped_private_messages: {type: Object},
recent_reactions: {type: Array}, recent_reactions: {type: Array},
is_administrator: {type: Boolean}, is_administrator: {type: Boolean},
stay_connected: {type: Boolean}, stay_connected: {type: Boolean},
progress: {type: Number},
}; };
} }
@@ -56,6 +58,7 @@ class TfElement extends LitElement {
tfrpc.rpc.getHash().then((hash) => self.set_hash(hash)); tfrpc.rpc.getHash().then((hash) => self.set_hash(hash));
tfrpc.register(function hashChanged(hash) { tfrpc.register(function hashChanged(hash) {
self.set_hash(hash); self.set_hash(hash);
self.reset_progress();
}); });
tfrpc.register(async function notifyNewMessage(id) { tfrpc.register(async function notifyNewMessage(id) {
await self.fetch_new_message(id); await self.fetch_new_message(id);
@@ -138,7 +141,9 @@ class TfElement extends LitElement {
'', '',
'@', '@',
'👍', '👍',
'🔐', ...Object.keys(this.grouped_private_messages)
.sort()
.map((x) => '🔐' + JSON.parse(x).join(',')),
...this.channels.map((x) => '#' + x), ...this.channels.map((x) => '#' + x),
]; ];
let index = channel_names.indexOf(this.hash.substring(1)); let index = channel_names.indexOf(this.hash.substring(1));
@@ -364,6 +369,32 @@ class TfElement extends LitElement {
return result; return result;
} }
async group_private_messages(messages) {
let groups = {};
let result = await this.decrypt(
await tfrpc.rpc.query(
`
SELECT messages.id, author, timestamp, json(content) AS content
FROM messages
JOIN json_each(?) AS ids
WHERE messages.id = ids.value
ORDER BY timestamp DESC
`,
[JSON.stringify(messages)]
)
);
for (let message of result) {
let key = JSON.stringify(
message?.decrypted?.recps?.filter((x) => x != this.whoami)?.sort()
);
if (!groups[key]) {
groups[key] = [];
}
groups[key].push(message);
}
return groups;
}
async load_channels_latest(following) { async load_channels_latest(following) {
let start_time = new Date(); let start_time = new Date();
let latest_private = this.get_latest_private(following); let latest_private = this.get_latest_private(following);
@@ -436,12 +467,15 @@ class TfElement extends LitElement {
console.log('channels took', (new Date() - start_time) / 1000.0); console.log('channels took', (new Date() - start_time) / 1000.0);
let self = this; let self = this;
start_time = new Date(); start_time = new Date();
latest_private.then(function (latest) { latest_private.then(async function (latest) {
self.channels_latest = Object.assign({}, self.channels_latest, { self.channels_latest = Object.assign({}, self.channels_latest, {
'🔐': latest[0], '🔐': latest[0],
}); });
console.log('private took', (new Date() - start_time) / 1000.0); console.log('private took', (new Date() - start_time) / 1000.0);
self.private_messages = latest[1]; self.private_messages = latest[1];
self.grouped_private_messages = await self.group_private_messages(
latest[1]
);
}); });
} }
@@ -450,7 +484,28 @@ class TfElement extends LitElement {
this.schedule_load_latest(); this.schedule_load_latest();
} }
reset_progress() {
if (this.progress === undefined) {
this._progress_start = new Date();
requestAnimationFrame(this.update_progress.bind(this));
}
}
update_progress() {
if (
!this.loading_latest &&
!this.loading_latest_scheduled &&
!this.shadowRoot.getElementById('tf-tab-news')?.is_loading()
) {
this.progress = undefined;
return;
}
this.progress = (new Date() - this._progress_start).valueOf();
requestAnimationFrame(this.update_progress.bind(this));
}
schedule_load_latest() { schedule_load_latest() {
this.reset_progress();
if (!this.loading_latest) { if (!this.loading_latest) {
this.shadowRoot.getElementById('tf-tab-news')?.load_latest(); this.shadowRoot.getElementById('tf-tab-news')?.load_latest();
this.load(); this.load();
@@ -495,6 +550,7 @@ class TfElement extends LitElement {
async load() { async load() {
this.loading_latest = true; this.loading_latest = true;
this.reset_progress();
try { try {
let start_time = new Date(); let start_time = new Date();
let whoami = this.whoami; let whoami = this.whoami;
@@ -603,8 +659,10 @@ class TfElement extends LitElement {
@channelsetunread=${this.channel_set_unread} @channelsetunread=${this.channel_set_unread}
@refresh=${this.refresh} @refresh=${this.refresh}
@toggle_stay_connected=${this.toggle_stay_connected} @toggle_stay_connected=${this.toggle_stay_connected}
@loadmessages=${this.reset_progress}
.connections=${this.connections} .connections=${this.connections}
.private_messages=${this.private_messages} .private_messages=${this.private_messages}
.grouped_private_messages=${this.grouped_private_messages}
.recent_reactions=${this.recent_reactions} .recent_reactions=${this.recent_reactions}
?is_administrator=${this.is_administrator} ?is_administrator=${this.is_administrator}
?stay_connected=${this.stay_connected} ?stay_connected=${this.stay_connected}
@@ -646,6 +704,7 @@ class TfElement extends LitElement {
async set_tab(tab) { async set_tab(tab) {
this.tab = tab; this.tab = tab;
if (tab === 'news') { if (tab === 'news') {
this.schedule_load_latest();
await tfrpc.rpc.setHash('#'); await tfrpc.rpc.setHash('#');
} else if (tab === 'connections') { } else if (tab === 'connections') {
await tfrpc.rpc.setHash('#connections'); await tfrpc.rpc.setHash('#connections');
@@ -751,11 +810,23 @@ class TfElement extends LitElement {
Loading... Loading...
</div>` </div>`
: this.render_tab(); : this.render_tab();
let progress =
this.progress !== undefined
? html`
<div style="position: absolute; width: 100%" id="progress">
<div
class="w3-theme-l3"
style=${`height: 4px; position: absolute; right: ${Math.cos(this.progress / 250) > 0 ? 'auto' : '0'}; width: ${50 * Math.sin(this.progress / 250) + 50}%`}
></div>
</div>
`
: undefined;
return html` return html`
<div <div
style="width: 100vw; min-height: 100vh; height: 100vh; display: flex; flex-direction: column" style="width: 100vw; min-height: 100vh; height: 100vh; display: flex; flex-direction: column"
class="w3-theme-dark" class="w3-theme-dark"
> >
${progress}
<div style="flex: 0 0">${tabs}</div> <div style="flex: 0 0">${tabs}</div>
<div style="flex: 1 1; overflow: auto; contain: layout"> <div style="flex: 1 1; overflow: auto; contain: layout">
${contents} ${contents}

View File

@@ -789,60 +789,45 @@ class TfMessageElement extends LitElement {
</div> </div>
`); `);
} else if (content.type == 'contact') { } else if (content.type == 'contact') {
return this.render_frame(html` switch (this.format) {
<div class="w3-bar"> case 'message':
<div class="w3-bar-item"> default:
<tf-user id=${this.message.author} .users=${this.users}></tf-user> return this.render_frame(html`
is <div class="w3-bar">
${content.blocking === true <div class="w3-bar-item">
? 'blocking' <tf-user
: content.blocking === false id=${this.message.author}
? 'no longer blocking' .users=${this.users}
: content.following === true ></tf-user>
? 'following' is
: content.following === false ${content.blocking === true
? 'no longer following' ? 'blocking'
: '?'} : content.blocking === false
<tf-user ? 'no longer blocking'
id=${this.message.content.contact} : content.following === true
.users=${this.users} ? 'following'
></tf-user> : content.following === false
</div> ? 'no longer following'
<div class="w3-bar-item w3-right"> : '?'}
<button class="w3-button w3-theme-d1" @click=${this.toggle_menu}> <tf-user
% id=${this.message.content.contact}
</button> .users=${this.users}
<div ></tf-user>
class="w3-dropdown-content w3-bar-block w3-card-4 w3-theme-l1" </div>
style="right: 48px" ${this.render_menu()} ${this.render_votes()}
> ${this.render_actions()}
<a
target="_top"
class="w3-button w3-bar-item"
href=${'#' + encodeURIComponent(this.message?.id)}
>View Message</a
>
<button
class="w3-button w3-bar-item w3-border-bottom"
@click=${this.copy_id}
>
Copy ID
</button>
${this.drafts[this.message?.id] === undefined
? html`
<button
class="w3-button w3-bar-item"
@click=${this.show_reply}
>
↩️ Reply
</button>
`
: undefined}
</div> </div>
`);
break;
case 'raw':
return this.render_frame(html`
${this.render_header()}
<div class="w3-container">${this.render_raw()}</div>
${this.render_votes()} ${this.render_actions()}
</div> </div>
${this.render_votes()} ${this.render_actions()} `);
</div> break;
`); }
} else if (content.type == 'post') { } else if (content.type == 'post') {
let self = this; let self = this;
let body; let body;

View File

@@ -18,6 +18,7 @@ class TfTabNewsFeedElement extends LitElement {
time_range: {type: Array}, time_range: {type: Array},
time_loading: {type: Array}, time_loading: {type: Array},
private_messages: {type: Array}, private_messages: {type: Array},
grouped_private_messages: {type: Object},
recent_reactions: {type: Array}, recent_reactions: {type: Array},
}; };
} }
@@ -106,6 +107,12 @@ class TfTabNewsFeedElement extends LitElement {
} }
async fetch_messages(start_time, end_time) { async fetch_messages(start_time, end_time) {
this.dispatchEvent(
new CustomEvent('loadmessages', {
bubbles: true,
composed: true,
})
);
this.time_loading = [start_time, end_time]; this.time_loading = [start_time, end_time];
let result; let result;
const k_max_results = 64; const k_max_results = 64;
@@ -221,6 +228,31 @@ class TfTabNewsFeedElement extends LitElement {
] ]
); );
result = (await this.decrypt(result)).filter((x) => x.decrypted); result = (await this.decrypt(result)).filter((x) => x.decrypted);
} else if (this.hash.startsWith('#🔐')) {
let ids = this.hash.substring('#🔐'.length).split(',');
console.log(this.grouped_private_messages);
result = await tfrpc.rpc.query(
`
SELECT TRUE AS is_primary, messages.rowid, messages.id, previous, author, sequence, timestamp, hash, json(content) AS content, signature
FROM messages
JOIN json_each(?1) AS private_messages ON messages.id = private_messages.value
WHERE
(?2 IS NULL OR (messages.timestamp >= ?2)) AND messages.timestamp < ?3 AND
json(messages.content) LIKE '"%'
ORDER BY messages.rowid DESC LIMIT ?4
`,
[
JSON.stringify(
this.grouped_private_messages?.[JSON.stringify(ids)]?.map(
(x) => x.id
) ?? []
),
start_time,
end_time,
k_max_results,
]
);
result = (await this.decrypt(result)).filter((x) => x.decrypted);
} else if (this.hash == '#👍') { } else if (this.hash == '#👍') {
result = await tfrpc.rpc.query( result = await tfrpc.rpc.query(
` `
@@ -378,7 +410,8 @@ class TfTabNewsFeedElement extends LitElement {
this.messages = []; this.messages = [];
this._messages_hash = this.hash; this._messages_hash = this.hash;
} }
this._messages_following = this.following; this._messages_following = JSON.stringify(this.following);
this._private_messages = JSON.stringify(this.private_messages);
let now = new Date().valueOf(); let now = new Date().valueOf();
let start_time = now - 24 * 60 * 60 * 1000; let start_time = now - 24 * 60 * 60 * 1000;
this.start_time = start_time; this.start_time = start_time;
@@ -421,8 +454,8 @@ class TfTabNewsFeedElement extends LitElement {
if ( if (
!this.messages || !this.messages ||
this._messages_hash !== this.hash || this._messages_hash !== this.hash ||
JSON.stringify(this._messages_following) !== this._messages_following !== JSON.stringify(this.following) ||
JSON.stringify(this.following) this._private_messages !== JSON.stringify(this.private_messages)
) { ) {
console.log( console.log(
`loading messages for ${this.whoami} (following ${this.following.length})` `loading messages for ${this.whoami} (following ${this.following.length})`

View File

@@ -24,6 +24,7 @@ class TfTabNewsElement extends LitElement {
channels_latest: {type: Object}, channels_latest: {type: Object},
connections: {type: Array}, connections: {type: Array},
private_messages: {type: Array}, private_messages: {type: Array},
grouped_private_messages: {type: Object},
recent_reactions: {type: Array}, recent_reactions: {type: Array},
peer_exchange: {type: Boolean}, peer_exchange: {type: Boolean},
is_administrator: {type: Boolean}, is_administrator: {type: Boolean},
@@ -180,6 +181,10 @@ class TfTabNewsElement extends LitElement {
await this.check_peer_exchange(); await this.check_peer_exchange();
} }
is_loading() {
return this.shadowRoot?.getElementById('news')?.loading;
}
render_sidebar() { render_sidebar() {
return html` return html`
<div <div
@@ -253,12 +258,28 @@ class TfTabNewsElement extends LitElement {
style=${this.hash == '#👍' ? 'font-weight: bold' : undefined} style=${this.hash == '#👍' ? 'font-weight: bold' : undefined}
>${this.unread_status('👍')}👍votes</a >${this.unread_status('👍')}👍votes</a
> >
<a ${Object.keys(this?.grouped_private_messages ?? [])
href="#🔐" ?.sort()
class="w3-bar-item w3-button" ?.map(
style=${this.hash == '#🔐' ? 'font-weight: bold' : undefined} (key) => html`
>${this.unread_status('🔐')}🔐private</a <a
> href=${'#🔐' + JSON.parse(key).join(',')}
class="w3-bar-item w3-button"
style=${this.hash == '#🔐' + JSON.parse(key).join(',')
? 'font-weight: bold'
: undefined}
>${(key != '[]' ? JSON.parse(key) : [this.whoami]).map(
(id) => html`
<tf-user
id=${id}
nolink="true"
.users=${this.users}
></tf-user>
`
)}</a
>
`
)}
${Object.keys(this.drafts) ${Object.keys(this.drafts)
.sort() .sort()
.map( .map(
@@ -430,6 +451,7 @@ class TfTabNewsElement extends LitElement {
.channels_unread=${this.channels_unread} .channels_unread=${this.channels_unread}
.channels_latest=${this.channels_latest} .channels_latest=${this.channels_latest}
.private_messages=${this.private_messages} .private_messages=${this.private_messages}
.grouped_private_messages=${this.grouped_private_messages}
.recent_reactions=${this.recent_reactions} .recent_reactions=${this.recent_reactions}
></tf-tab-news-feed> ></tf-tab-news-feed>
</div> </div>

View File

@@ -9,6 +9,7 @@ class TfUserElement extends LitElement {
fallback_name: {type: String}, fallback_name: {type: String},
icon_only: {type: Boolean}, icon_only: {type: Boolean},
users: {type: Object}, users: {type: Object},
nolink: {type: Boolean},
}; };
} }
@@ -37,7 +38,9 @@ class TfUserElement extends LitElement {
let name_string = name ?? this.fallback_name ?? this.id; let name_string = name ?? this.fallback_name ?? this.id;
name = this.icon_only name = this.icon_only
? undefined ? undefined
: html`<a target="_top" href=${'#' + this.id}>${name_string}</a>`; : !this.nolink
? html`<a target="_top" href=${'#' + this.id}>${name_string}</a>`
: html`<span>${name_string}</span>`;
if (user) { if (user) {
let image_link = user.image; let image_link = user.image;
@@ -56,7 +59,8 @@ class TfUserElement extends LitElement {
} }
} }
return html` <div return html` <div
style="display: inline-block; vertical-align: middle; font-weight: bold; text-wrap: nowrap; max-width: 100%; overflow: hidden; text-overflow: ellipsis" style=${'display: inline-block; vertical-align: middle; text-wrap: nowrap; max-width: 100%; overflow: hidden; text-overflow: ellipsis' +
(this.nolink ? '' : '; font-weight: bold')}
> >
${image} ${name} ${image} ${name}
</div>`; </div>`;

View File

@@ -1,7 +1,23 @@
/**
* \file
* \defgroup tfapp Tilde Friends App JS
* Tilde Friends server-side app wrapper.
* @{
*/
/** \cond */
import * as core from './core.js'; import * as core from './core.js';
let gSessionIndex = 0; export {App};
/** \endcond */
/** A sequence number of apps. */
let g_session_index = 0;
/**
** App constructor.
** @return An app instance.
*/
function App() { function App() {
this._send_queue = []; this._send_queue = [];
this.calls = {}; this.calls = {};
@@ -9,6 +25,12 @@ function App() {
return this; return this;
} }
/**
** Create a function wrapper that when called invokes a function on the app
** itself.
** @param api The function and argument names.
** @return A function.
*/
App.prototype.makeFunction = function (api) { App.prototype.makeFunction = function (api) {
let self = this; let self = this;
let result = function () { let result = function () {
@@ -32,6 +54,10 @@ App.prototype.makeFunction = function (api) {
return result; return result;
}; };
/**
** Send a message to the app.
** @param message The message to send.
*/
App.prototype.send = function (message) { App.prototype.send = function (message) {
if (this._send_queue) { if (this._send_queue) {
if (this._on_output) { if (this._on_output) {
@@ -46,6 +72,11 @@ App.prototype.send = function (message) {
} }
}; };
/**
** App socket handler.
** @param request The HTTP request of the WebSocket connection.
** @param response The HTTP response.
*/
exports.app_socket = async function socket(request, response) { exports.app_socket = async function socket(request, response) {
let process; let process;
let options = {}; let options = {};
@@ -133,7 +164,7 @@ exports.app_socket = async function socket(request, response) {
options.packageOwner = packageOwner; options.packageOwner = packageOwner;
options.packageName = packageName; options.packageName = packageName;
options.url = message.url; options.url = message.url;
let sessionId = 'session_' + (gSessionIndex++).toString(); let sessionId = 'session_' + (g_session_index++).toString();
if (blobId) { if (blobId) {
if (message.edit_only) { if (message.edit_only) {
response.send( response.send(
@@ -218,4 +249,4 @@ exports.app_socket = async function socket(request, response) {
response.upgrade(100, {}); response.upgrade(100, {});
}; };
export {App}; /** @} */

View File

@@ -72,7 +72,7 @@ class TfNavigationElement extends LitElement {
} }
/** /**
* TODOC * Toggle editor visibility.
* @param event The HTML event. * @param event The HTML event.
*/ */
toggle_edit(event) { toggle_edit(event) {
@@ -85,7 +85,7 @@ class TfNavigationElement extends LitElement {
} }
/** /**
* TODOC * Remove a stored permission.
* @param key The permission to reset. * @param key The permission to reset.
*/ */
reset_permission(key) { reset_permission(key) {
@@ -93,7 +93,7 @@ class TfNavigationElement extends LitElement {
} }
/** /**
* TODOC * Get or create a spark line.
* @param key The spark line identifier. * @param key The spark line identifier.
* @param options Spark line options. * @param options Spark line options.
* @return A spark line HTML element. * @return A spark line HTML element.
@@ -262,8 +262,8 @@ class TfNavigationElement extends LitElement {
} }
/** /**
* TODOC * Render the permissions popup.
* @returns * @return Lit HTML.
*/ */
render_permissions() { render_permissions() {
if (this.show_permissions) { if (this.show_permissions) {
@@ -312,8 +312,8 @@ class TfNavigationElement extends LitElement {
} }
/** /**
* TODOC * Render the navigation bar.
* @returns * @return Lit HTML.
*/ */
render() { render() {
let self = this; let self = this;
@@ -441,7 +441,7 @@ class TfNavigationElement extends LitElement {
customElements.define('tf-navigation', TfNavigationElement); customElements.define('tf-navigation', TfNavigationElement);
/** /**
* TODOC * A file in the files sidebar.
*/ */
class TfFilesElement extends LitElement { class TfFilesElement extends LitElement {
/** /**
@@ -467,7 +467,7 @@ class TfFilesElement extends LitElement {
} }
/** /**
* TODOC * Select a clicked file.
* @param file The file. * @param file The file.
*/ */
file_click(file) { file_click(file) {
@@ -483,9 +483,9 @@ class TfFilesElement extends LitElement {
} }
/** /**
* TODOC * Render a single file in the file list.
* @param file The file. * @param file The file.
* @returns Lit HTML. * @return Lit HTML.
*/ */
render_file(file) { render_file(file) {
let classes = ['file']; let classes = ['file'];
@@ -507,7 +507,7 @@ class TfFilesElement extends LitElement {
} }
/** /**
* TODOC * Create a file entry for a dropped file.
* @param event The event. * @param event The event.
*/ */
async drop(event) { async drop(event) {
@@ -533,7 +533,7 @@ class TfFilesElement extends LitElement {
} }
/** /**
* TODOC * Called when a file starts being dragged over the file.
* @param event The event. * @param event The event.
*/ */
drag_enter(event) { drag_enter(event) {
@@ -543,7 +543,7 @@ class TfFilesElement extends LitElement {
} }
/** /**
* TODOC * Called when a file stops being dragged over the file.
* @param event The event. * @param event The event.
*/ */
drag_leave(event) { drag_leave(event) {
@@ -554,7 +554,7 @@ class TfFilesElement extends LitElement {
} }
/** /**
* Drag over event. * Called when a file is being dragged over the file.
* @param event The event. * @param event The event.
*/ */
drag_over(event) { drag_over(event) {
@@ -562,8 +562,8 @@ class TfFilesElement extends LitElement {
} }
/** /**
* TODOC * Render the file.
* @returns * @return Lit HTML.
*/ */
render() { render() {
let self = this; let self = this;
@@ -610,7 +610,7 @@ class TfFilesElement extends LitElement {
customElements.define('tf-files', TfFilesElement); customElements.define('tf-files', TfFilesElement);
/** /**
* TODOC * The files pane element.
*/ */
class TfFilesPaneElement extends LitElement { class TfFilesPaneElement extends LitElement {
/** /**
@@ -635,7 +635,7 @@ class TfFilesPaneElement extends LitElement {
} }
/** /**
* TODOC * Set whether the files pane is expanded.
* @param expanded Whether the files pane is expanded. * @param expanded Whether the files pane is expanded.
*/ */
set_expanded(expanded) { set_expanded(expanded) {
@@ -644,8 +644,8 @@ class TfFilesPaneElement extends LitElement {
} }
/** /**
* TODOC * Render the files pane element.
* @returns * @return Lit HTML.
*/ */
render() { render() {
let self = this; let self = this;
@@ -704,7 +704,7 @@ class TfFilesPaneElement extends LitElement {
customElements.define('tf-files-pane', TfFilesPaneElement); customElements.define('tf-files-pane', TfFilesPaneElement);
/** /**
* TODOC * A tiny graph.
*/ */
class TfSparkLineElement extends LitElement { class TfSparkLineElement extends LitElement {
static get properties() { static get properties() {
@@ -724,9 +724,9 @@ class TfSparkLineElement extends LitElement {
} }
/** /**
* TODOC * Add a data point to the graph.
* @param {*} key * @param key The line to which the point applies.
* @param {*} value * @param value The numeric value of the data point.
*/ */
append(key, value) { append(key, value) {
let line = null; let line = null;
@@ -753,9 +753,9 @@ class TfSparkLineElement extends LitElement {
} }
/** /**
* TODOC * Render a single series line.
* @param {*} line * @param line The line data.
* @returns * @return Lit HTML.
*/ */
render_line(line) { render_line(line) {
if (line?.values?.length >= 2) { if (line?.values?.length >= 2) {
@@ -771,8 +771,8 @@ class TfSparkLineElement extends LitElement {
} }
/** /**
* TODOC * Render the graph.
* @returns * @return Lit HTML.
*/ */
render() { render() {
let max = let max =
@@ -799,7 +799,9 @@ class TfSparkLineElement extends LitElement {
customElements.define('tf-sparkline', TfSparkLineElement); customElements.define('tf-sparkline', TfSparkLineElement);
// TODOC /**
* A keyboard key is pressed down.
*/
window.addEventListener('keydown', function (event) { window.addEventListener('keydown', function (event) {
if (event.keyCode == 83 && (event.altKey || event.ctrlKey)) { if (event.keyCode == 83 && (event.altKey || event.ctrlKey)) {
if (editing()) { if (editing()) {
@@ -860,24 +862,23 @@ function ensureLoaded(nodes, callback) {
} }
/** /**
* TODOC * Check whether the editior is currently visible.
* @returns * @return true if the editor is visible.
*/ */
function editing() { function editing() {
return document.getElementById('editPane').style.display != 'none'; return document.getElementById('editPane').style.display != 'none';
} }
/** /**
* TODOC * Check whether only the editor is visible and the app is hidden.
* @returns * @return true if the editor is visible and the app is not.
*/ */
function is_edit_only() { function is_edit_only() {
return window.location.search == '?editonly=1' || window.innerWidth < 1024; return window.location.search == '?editonly=1' || window.innerWidth < 1024;
} }
/** /**
* TODOC * Show the editor.
* @returns
*/ */
async function edit() { async function edit() {
if (editing()) { if (editing()) {
@@ -904,7 +905,7 @@ async function edit() {
} }
/** /**
* TODOC * Open a performance trace.
*/ */
function trace() { function trace() {
window.open(`/speedscope/#profileURL=${encodeURIComponent('/trace')}`); window.open(`/speedscope/#profileURL=${encodeURIComponent('/trace')}`);
@@ -982,7 +983,7 @@ async function load(path) {
} }
/** /**
* TODOC * Hide the editor.
*/ */
function closeEditor() { function closeEditor() {
window.localStorage.setItem('editing', '0'); window.localStorage.setItem('editing', '0');
@@ -990,14 +991,6 @@ function closeEditor() {
document.getElementById('viewPane').style.display = 'flex'; document.getElementById('viewPane').style.display = 'flex';
} }
/**
* TODOC
* @returns
*/
function explodePath() {
return /^\/~([^\/]+)\/([^\/]+)(.*)/.exec(window.location.pathname);
}
/** /**
* Save the app. * Save the app.
* @param save_to An optional path to which to save the app. * @param save_to An optional path to which to save the app.
@@ -1111,7 +1104,7 @@ function save(save_to) {
} }
/** /**
* TODOC * Prompt to set the app icon.
*/ */
function changeIcon() { function changeIcon() {
let value = prompt('Enter a new app icon emoji:'); let value = prompt('Enter a new app icon emoji:');
@@ -1122,7 +1115,7 @@ function changeIcon() {
} }
/** /**
* TODOC * Prompt to delete the current app.
*/ */
function deleteApp() { function deleteApp() {
let name = document.getElementById('name'); let name = document.getElementById('name');
@@ -1143,8 +1136,8 @@ function deleteApp() {
} }
/** /**
* TODOC * Get the current app URL.
* @returns * @return The app URL.
*/ */
function url() { function url() {
let hash = window.location.href.indexOf('#'); let hash = window.location.href.indexOf('#');
@@ -1162,8 +1155,8 @@ function url() {
} }
/** /**
* TODOC * Get the window hash without the lone '#' if it is empty.
* @returns * @return The hash.
*/ */
function hash() { function hash() {
return window.location.hash != '#' ? window.location.hash : ''; return window.location.hash != '#' ? window.location.hash : '';
@@ -1188,7 +1181,7 @@ function api_postMessage(message) {
} }
/** /**
* TODOC * Show an error.
* @param error The error. * @param error The error.
*/ */
function api_error(error) { function api_error(error) {
@@ -1293,7 +1286,7 @@ function api_requestPermission(permission, id) {
} }
/** /**
* TODOC * Log from the app to the console.
*/ */
function api_print() { function api_print() {
console.log('app>', ...arguments); console.log('app>', ...arguments);
@@ -1308,7 +1301,7 @@ function api_setHash(hash) {
} }
/** /**
* TODOC * Process an incoming WebSocket message.
* @param message The message. * @param message The message.
*/ */
function _receive_websocket_message(message) { function _receive_websocket_message(message) {
@@ -1432,14 +1425,14 @@ function send(value) {
} }
/** /**
* TODOC * Notify the app of the window hash changing.
*/ */
function hashChange() { function hashChange() {
send({event: 'hashChange', hash: window.location.hash}); send({event: 'hashChange', hash: window.location.hash});
} }
/** /**
* TODOC * Make sure the app is connected on window focus, and notify the app.
*/ */
function focus() { function focus() {
if (gSocket && gSocket.readyState == gSocket.CLOSED) { if (gSocket && gSocket.readyState == gSocket.CLOSED) {
@@ -1450,7 +1443,7 @@ function focus() {
} }
/** /**
* TODOC * Notify the app of lost focus.
*/ */
function blur() { function blur() {
if (gSocket && gSocket.readyState == gSocket.OPEN) { if (gSocket && gSocket.readyState == gSocket.OPEN) {
@@ -1617,7 +1610,7 @@ function openFile(name) {
} }
/** /**
* TODOC * Refresh the files list.
*/ */
function updateFiles() { function updateFiles() {
let files = document.getElementsByTagName('tf-files-pane')[0]; let files = document.getElementsByTagName('tf-files-pane')[0];
@@ -1650,7 +1643,7 @@ function makeNewFile(name) {
} }
/** /**
* TODOC * Prompt to create a new file.
*/ */
function newFile() { function newFile() {
let name = prompt('Name of new file:', 'file.js'); let name = prompt('Name of new file:', 'file.js');
@@ -1660,7 +1653,7 @@ function newFile() {
} }
/** /**
* TODOC * Prompt to remove a file.
*/ */
function removeFile() { function removeFile() {
if (confirm('Remove ' + gCurrentFile + '?')) { if (confirm('Remove ' + gCurrentFile + '?')) {
@@ -1670,7 +1663,7 @@ function removeFile() {
} }
/** /**
* TODOC * Export the app to a zip file, which is downloaded by the browser.
*/ */
async function appExport() { async function appExport() {
let JsZip = (await import('/static/jszip.min.js')).default; let JsZip = (await import('/static/jszip.min.js')).default;
@@ -1728,7 +1721,7 @@ async function save_file_to_blob_id(name, file) {
} }
/** /**
* TODOC * Prompt to import an app from a zip file.
*/ */
async function appImport() { async function appImport() {
let JsZip = (await import('/static/jszip.min.js')).default; let JsZip = (await import('/static/jszip.min.js')).default;
@@ -1855,7 +1848,9 @@ function toggleVisibleWhitespace() {
} }
} }
// TODOC /**
* Register event handlers and connect the WebSocket on load.
*/
window.addEventListener('load', function () { window.addEventListener('load', function () {
window.addEventListener('hashchange', hashChange); window.addEventListener('hashchange', hashChange);
window.addEventListener('focus', focus); window.addEventListener('focus', focus);

View File

@@ -1,8 +1,14 @@
/** /**
* TODOC * \file
* TODO: document so we can improve this * \defgroup tfhttp Tilde Friends HTTP Client JS
* @param {*} url * Tilde Friends server-side HTTP client.
* @returns * @{
*/
/**
* Parse a URL into protocol, host, path, and port parts.
* @param url
* @return An object of the URL parts.
*/ */
function parseUrl(url) { function parseUrl(url) {
// XXX: Hack. // XXX: Hack.
@@ -16,9 +22,9 @@ function parseUrl(url) {
} }
/** /**
* TODOC * Parse an HTTP response into headers and body content.
* @param {*} data * @param data The response data, headers and body included.
* @returns * @return headers and body data.
*/ */
function parseResponse(data) { function parseResponse(data) {
let firstLine; let firstLine;
@@ -36,15 +42,15 @@ function parseResponse(data) {
headers[line.substring(colon)] = line.substring(colon + 1); headers[line.substring(colon)] = line.substring(colon + 1);
} }
} }
return {body: data}; return {headers: headers, body: data};
} }
/** /**
* TODOC * Make an HTTP request.
* @param {*} url * @param url The URL.
* @param {*} options * @param options Request options.
* @param {*} allowed_hosts * @param allowed_hosts List of allowed hosts.
* @returns * @return A promise resolved with the response headers and body.
*/ */
export function fetch(url, options, allowed_hosts) { export function fetch(url, options, allowed_hosts) {
let parsed = parseUrl(url); let parsed = parseUrl(url);
@@ -111,3 +117,5 @@ export function fetch(url, options, allowed_hosts) {
}); });
}); });
} }
/** @} */

View File

@@ -15,8 +15,8 @@ let g_next_id = 1;
let g_calls = {}; let g_calls = {};
/** /**
* TODOC * Check if being called from a browser vs. server-side.
* @returns * @return true if called from a browser.
*/ */
function get_is_browser() { function get_is_browser() {
try { try {

View File

@@ -25,14 +25,14 @@
}: }:
pkgs.stdenv.mkDerivation rec { pkgs.stdenv.mkDerivation rec {
pname = "tildefriends"; pname = "tildefriends";
version = "0.0.32"; version = "0.0.33";
src = pkgs.fetchFromGitea { src = pkgs.fetchFromGitea {
domain = "dev.tildefriends.net"; domain = "dev.tildefriends.net";
owner = "cory"; owner = "cory";
repo = "tildefriends"; repo = "tildefriends";
rev = "v${version}"; rev = "v${version}";
hash = "sha256-Dk0NOEQIg2LeENySK0+MgpZEtfsClGq6dZL+eOOpE0U="; hash = "sha256-9D28gmaBTRVyXhY3zZd/W9PsXA1YZt/K69hz41aVP04=";
fetchSubmodules = true; fetchSubmodules = true;
}; };

View File

@@ -11,7 +11,7 @@
<link rel="icon" type="image/x-icon" href="favicon-FOKUP5Y5.ico"> <link rel="icon" type="image/x-icon" href="favicon-FOKUP5Y5.ico">
</head> </head>
<body> <body>
<script src="speedscope-7YPLLUY2.js"></script> <script src="speedscope-HCR63FMT.js"></script>

View File

@@ -1,3 +1,3 @@
speedscope@1.23.0 speedscope@1.23.1
Sun Jul 6 20:04:28 PDT 2025 Mon Aug 11 11:43:09 PDT 2025
aa9bef50789a2989746b576fff182b6f01dfce6a 0cec0f82c334aed6cf19d43cabeadcda0d95e0fc

File diff suppressed because one or more lines are too long

6
flake.lock generated
View File

@@ -20,11 +20,11 @@
}, },
"nixpkgs": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 1750622754, "lastModified": 1753749649,
"narHash": "sha256-kMhs+YzV4vPGfuTpD3mwzibWUE6jotw5Al2wczI0Pv8=", "narHash": "sha256-+jkEZxs7bfOKfBIk430K+tK9IvXlwzqQQnppC2ZKFj4=",
"owner": "NixOS", "owner": "NixOS",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "c7ab75210cb8cb16ddd8f290755d9558edde7ee1", "rev": "1f08a4df998e21f4e8be8fb6fbf61d11a1a5076a",
"type": "github" "type": "github"
}, },
"original": { "original": {

View File

@@ -1,8 +1,8 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.unprompted.tildefriends" package="com.unprompted.tildefriends"
android:versionCode="40" android:versionCode="41"
android:versionName="0.0.33"> android:versionName="0.2025.8-wip">
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<uses-permission android:name="android.permission.INTERNET"/> <uses-permission android:name="android.permission.INTERNET"/>
<application <application

View File

@@ -81,14 +81,14 @@ public class TildeFriendsActivity extends Activity {
TildeFriendsActivity activity = this; TildeFriendsActivity activity = this;
Log.w("tildefriends", "Watching for changes in: " + getFilesDir().toString());
observer = make_file_observer(getFilesDir().toString(), port_file_path);
observer.startWatching();
set_status("Starting server..."); set_status("Starting server...");
server_thread = new Thread(new Runnable() { server_thread = new Thread(new Runnable() {
@Override @Override
public void run() { public void run() {
Log.w("tildefriends", "Watching for changes in: " + getFilesDir().toString());
observer = make_file_observer(getFilesDir().toString(), port_file_path);
observer.startWatching();
Log.w("tildefriends", "Calling tf_server_main."); Log.w("tildefriends", "Calling tf_server_main.");
int result = tf_server_main( int result = tf_server_main(
getFilesDir().toString(), getFilesDir().toString(),
@@ -426,7 +426,7 @@ public class TildeFriendsActivity extends Activity {
Log.w("tildefriends", "onServiceDisconnected"); Log.w("tildefriends", "onServiceDisconnected");
} }
}; };
s_activity.bindService(intent, s_activity.service_connection, BIND_AUTO_CREATE); s_activity.bindService(intent, s_activity.service_connection, BIND_AUTO_CREATE | BIND_NOT_FOREGROUND);
} }
public static void stop_sandbox() { public static void stop_sandbox() {
@@ -445,6 +445,7 @@ public class TildeFriendsActivity extends Activity {
hide_status(); hide_status();
web_view.loadUrl(base_url + "login/auto"); web_view.loadUrl(base_url + "login/auto");
}); });
observer.stopWatching();
observer = null; observer = null;
} else { } else {
runOnUiThread(() -> { runOnUiThread(() -> {

View File

@@ -13,13 +13,13 @@
<key>CFBundlePackageType</key> <key>CFBundlePackageType</key>
<string>APPL</string> <string>APPL</string>
<key>CFBundleShortVersionString</key> <key>CFBundleShortVersionString</key>
<string>0.0.33</string> <string>0.2025.8</string>
<key>CFBundleSupportedPlatforms</key> <key>CFBundleSupportedPlatforms</key>
<array> <array>
<string>iPhoneOS</string> <string>iPhoneOS</string>
</array> </array>
<key>CFBundleVersion</key> <key>CFBundleVersion</key>
<string>15</string> <string>16</string>
<key>DTPlatformName</key> <key>DTPlatformName</key>
<string>iphoneos</string> <string>iphoneos</string>
<key>LSRequiresIPhoneOS</key> <key>LSRequiresIPhoneOS</key>

View File

@@ -133,6 +133,15 @@ typedef struct _tf_ssb_message_added_callback_node_t
tf_ssb_message_added_callback_node_t* next; tf_ssb_message_added_callback_node_t* next;
} tf_ssb_message_added_callback_node_t; } tf_ssb_message_added_callback_node_t;
typedef struct _tf_ssb_blob_stored_callback_node_t tf_ssb_blob_stored_callback_node_t;
typedef struct _tf_ssb_blob_stored_callback_node_t
{
tf_ssb_blob_stored_callback_t* callback;
tf_ssb_callback_cleanup_t* cleanup;
void* user_data;
tf_ssb_blob_stored_callback_node_t* next;
} tf_ssb_blob_stored_callback_node_t;
typedef struct _tf_ssb_blob_want_added_callback_node_t tf_ssb_blob_want_added_callback_node_t; typedef struct _tf_ssb_blob_want_added_callback_node_t tf_ssb_blob_want_added_callback_node_t;
typedef struct _tf_ssb_blob_want_added_callback_node_t typedef struct _tf_ssb_blob_want_added_callback_node_t
{ {
@@ -235,6 +244,9 @@ typedef struct _tf_ssb_t
tf_ssb_message_added_callback_node_t* message_added; tf_ssb_message_added_callback_node_t* message_added;
int message_added_count; int message_added_count;
tf_ssb_blob_stored_callback_node_t* blob_stored;
int blob_stored_count;
tf_ssb_blob_want_added_callback_node_t* blob_want_added; tf_ssb_blob_want_added_callback_node_t* blob_want_added;
int blob_want_added_count; int blob_want_added_count;
@@ -2741,6 +2753,17 @@ void tf_ssb_destroy(tf_ssb_t* ssb)
} }
tf_free(node); tf_free(node);
} }
while (ssb->blob_stored)
{
tf_ssb_blob_stored_callback_node_t* node = ssb->blob_stored;
ssb->blob_stored = node->next;
ssb->blob_stored_count--;
if (node->cleanup)
{
node->cleanup(ssb, node->user_data);
}
tf_free(node);
}
while (ssb->blob_want_added) while (ssb->blob_want_added)
{ {
tf_ssb_blob_want_added_callback_node_t* node = ssb->blob_want_added; tf_ssb_blob_want_added_callback_node_t* node = ssb->blob_want_added;
@@ -3960,9 +3983,53 @@ void tf_ssb_remove_message_added_callback(tf_ssb_t* ssb, tf_ssb_message_added_ca
} }
} }
void tf_ssb_add_blob_stored_callback(tf_ssb_t* ssb, tf_ssb_blob_stored_callback_t* callback, void (*cleanup)(tf_ssb_t* ssb, void* user_data), void* user_data)
{
tf_ssb_blob_stored_callback_node_t* node = tf_malloc(sizeof(tf_ssb_blob_stored_callback_node_t));
*node = (tf_ssb_blob_stored_callback_node_t) {
.callback = callback,
.cleanup = cleanup,
.user_data = user_data,
.next = ssb->blob_stored,
};
ssb->blob_stored = node;
ssb->blob_stored_count++;
}
void tf_ssb_remove_blob_stored_callback(tf_ssb_t* ssb, tf_ssb_blob_stored_callback_t* callback, void* user_data)
{
tf_ssb_blob_stored_callback_node_t** it = &ssb->blob_stored;
while (*it)
{
if ((*it)->callback == callback && (*it)->user_data == user_data)
{
tf_ssb_blob_stored_callback_node_t* node = *it;
*it = node->next;
ssb->blob_stored_count--;
if (node->cleanup)
{
node->cleanup(ssb, node->user_data);
}
tf_free(node);
}
else
{
it = &(*it)->next;
}
}
}
void tf_ssb_notify_blob_stored(tf_ssb_t* ssb, const char* id) void tf_ssb_notify_blob_stored(tf_ssb_t* ssb, const char* id)
{ {
tf_ssb_blob_stored_callback_node_t* next = NULL;
ssb->blobs_stored++; ssb->blobs_stored++;
for (tf_ssb_blob_stored_callback_node_t* node = ssb->blob_stored; node; node = next)
{
next = node->next;
tf_trace_begin(ssb->trace, "blob stored callback");
node->callback(ssb, id, node->user_data);
tf_trace_end(ssb->trace);
}
} }
void tf_ssb_notify_message_added(tf_ssb_t* ssb, const char* author, int32_t sequence, const char* id, JSValue message_keys) void tf_ssb_notify_message_added(tf_ssb_t* ssb, const char* author, int32_t sequence, const char* id, JSValue message_keys)

View File

@@ -680,6 +680,31 @@ void tf_ssb_remove_message_added_callback(tf_ssb_t* ssb, tf_ssb_message_added_ca
*/ */
void tf_ssb_notify_message_added(tf_ssb_t* ssb, const char* author, int32_t sequence, const char* id, JSValue message_with_keys); void tf_ssb_notify_message_added(tf_ssb_t* ssb, const char* author, int32_t sequence, const char* id, JSValue message_with_keys);
/**
** A callback called when a blob is added to the database.
** @param ssb The SSB instance.
** @param id The blob identifier.
** @param user_data The user data.
*/
typedef void(tf_ssb_blob_stored_callback_t)(tf_ssb_t* ssb, const char* id, void* user_data);
/**
** Register a callback called when a blob is added to the database.
** @param ssb The SSB instance.
** @param callback The callback function.
** @param cleanup A function to call when the callback is removed.
** @param user_data User data to pass to the callback.
*/
void tf_ssb_add_blob_stored_callback(tf_ssb_t* ssb, tf_ssb_blob_stored_callback_t* callback, tf_ssb_callback_cleanup_t* cleanup, void* user_data);
/**
** Remove a callback registered for when a blob is added to the database.
** @param ssb The SSB instance.
** @param callback The callback function.
** @param user_data User data registered with the callback.
*/
void tf_ssb_remove_blob_stored_callback(tf_ssb_t* ssb, tf_ssb_blob_stored_callback_t* callback, void* user_data);
/** /**
** Record that a new blob was stored. ** Record that a new blob was stored.
** @param ssb The SSB instance. ** @param ssb The SSB instance.

View File

@@ -1627,6 +1627,20 @@ static void _tf_ssb_on_message_added_callback(tf_ssb_t* ssb, const char* author,
JS_FreeValue(context, string); JS_FreeValue(context, string);
} }
static void _tf_ssb_on_blob_stored_callback(tf_ssb_t* ssb, const char* id, void* user_data)
{
JSContext* context = tf_ssb_get_context(ssb);
JSValue callback = JS_MKPTR(JS_TAG_OBJECT, user_data);
JSValue string = JS_NewString(context, id);
JSValue response = JS_Call(context, callback, JS_UNDEFINED, 1, &string);
if (tf_util_report_error(context, response))
{
tf_ssb_remove_blob_stored_callback(ssb, _tf_ssb_on_blob_stored_callback, user_data);
}
JS_FreeValue(context, response);
JS_FreeValue(context, string);
}
static void _tf_ssb_on_blob_want_added_callback(tf_ssb_t* ssb, const char* id, void* user_data) static void _tf_ssb_on_blob_want_added_callback(tf_ssb_t* ssb, const char* id, void* user_data)
{ {
JSContext* context = tf_ssb_get_context(ssb); JSContext* context = tf_ssb_get_context(ssb);
@@ -1747,6 +1761,11 @@ static JSValue _tf_ssb_add_event_listener(JSContext* context, JSValueConst this_
void* ptr = JS_VALUE_GET_PTR(JS_DupValue(context, callback)); void* ptr = JS_VALUE_GET_PTR(JS_DupValue(context, callback));
tf_ssb_add_message_added_callback(ssb, _tf_ssb_on_message_added_callback, _tf_ssb_cleanup_value, ptr); tf_ssb_add_message_added_callback(ssb, _tf_ssb_on_message_added_callback, _tf_ssb_cleanup_value, ptr);
} }
else if (strcmp(event_name, "blob") == 0)
{
void* ptr = JS_VALUE_GET_PTR(JS_DupValue(context, callback));
tf_ssb_add_blob_stored_callback(ssb, _tf_ssb_on_blob_stored_callback, _tf_ssb_cleanup_value, ptr);
}
else if (strcmp(event_name, "blob_want_added") == 0) else if (strcmp(event_name, "blob_want_added") == 0)
{ {
void* ptr = JS_VALUE_GET_PTR(JS_DupValue(context, callback)); void* ptr = JS_VALUE_GET_PTR(JS_DupValue(context, callback));
@@ -1790,6 +1809,11 @@ static JSValue _tf_ssb_remove_event_listener(JSContext* context, JSValueConst th
void* ptr = JS_VALUE_GET_PTR(JS_DupValue(context, callback)); void* ptr = JS_VALUE_GET_PTR(JS_DupValue(context, callback));
tf_ssb_remove_message_added_callback(ssb, _tf_ssb_on_message_added_callback, ptr); tf_ssb_remove_message_added_callback(ssb, _tf_ssb_on_message_added_callback, ptr);
} }
else if (strcmp(event_name, "blob") == 0)
{
void* ptr = JS_VALUE_GET_PTR(JS_DupValue(context, callback));
tf_ssb_remove_blob_stored_callback(ssb, _tf_ssb_on_blob_stored_callback, ptr);
}
else if (strcmp(event_name, "blob_want_added") == 0) else if (strcmp(event_name, "blob_want_added") == 0)
{ {
void* ptr = JS_VALUE_GET_PTR(JS_DupValue(context, callback)); void* ptr = JS_VALUE_GET_PTR(JS_DupValue(context, callback));

View File

@@ -152,6 +152,12 @@ static void _wait_stored(tf_ssb_t* ssb, bool* stored)
} }
} }
static void _blob_stored(tf_ssb_t* ssb, const char* id, void* user_data)
{
tf_printf("blob stored %s\n", id);
*(bool*)user_data = true;
}
void tf_ssb_test_ssb(const tf_test_options_t* options) void tf_ssb_test_ssb(const tf_test_options_t* options)
{ {
tf_printf("Testing SSB.\n"); tf_printf("Testing SSB.\n");
@@ -224,8 +230,13 @@ void tf_ssb_test_ssb(const tf_test_options_t* options)
char blob_id[k_id_base64_len] = { 0 }; char blob_id[k_id_base64_len] = { 0 };
const char* k_blob = "Hello, blob!"; const char* k_blob = "Hello, blob!";
bool blob_stored = false;
tf_ssb_add_blob_stored_callback(ssb0, _blob_stored, NULL, &blob_stored);
b = tf_ssb_db_blob_store(ssb0, (const uint8_t*)k_blob, strlen(k_blob), blob_id, sizeof(blob_id), NULL); b = tf_ssb_db_blob_store(ssb0, (const uint8_t*)k_blob, strlen(k_blob), blob_id, sizeof(blob_id), NULL);
tf_ssb_notify_blob_stored(ssb0, blob_id);
tf_ssb_remove_blob_stored_callback(ssb0, _blob_stored, &blob_stored);
assert(b); assert(b);
assert(blob_stored);
JSContext* context0 = tf_ssb_get_context(ssb0); JSContext* context0 = tf_ssb_get_context(ssb0);
JSValue obj = JS_NewObject(context0); JSValue obj = JS_NewObject(context0);

View File

@@ -1,2 +1,2 @@
#define VERSION_NUMBER "0.0.33" #define VERSION_NUMBER "0.2025.8-wip"
#define VERSION_NAME "This program kills fascists." #define VERSION_NAME "This program kills fascists."