Commit Graph
305 Commits
Author SHA1 Message Date
evgeny a0049a4187 b64 prefix: * marker to distinguish base64-encoded display data from old plain text
* prefix prepended in on_media_registered before b64 data
on_msg_inserted, chat_core_attachment_download, get_messages_json
only attempt b64_decode when prefix starts with '*' marker
2026-08-03 23:10:15 +03:00
evgeny ea6eeae210 msg b64: base64-encode display prefix in d-field to avoid pipe/quote breaking parse
b64_encode/decode (URL-safe A-Za-z0-9-_) in chat_core_priv.h
on_media_registered encodes msg_data before full_data
on_msg_inserted/chat_core_attachment_download decode base_filename
android_jni_bridge decodes prefix for UI display
2026-08-03 23:02:49 +03:00
evgeny 6a02a16543 attachments: preview bar + combined send (Android + desktop)
Android: file picker → pending preview bar (name+size+X) →
         combined send with text; cache copy, save dialog for downloads

Desktop: attach stores pending, preview bar before send,
         combined with text in onSendClicked

C: attachment_sender.c — include filename in msg data for display
2026-08-03 22:36:33 +03:00
evgeny e3df5008da etcp: fire link_status_cbk in etcp_link_new for new links 2026-08-03 21:46:22 +03:00
evgeny 13c6ef2210 member_sync: fix p-6 for ip pointer, add stcp link addr+status to JNI bridge 2026-08-03 21:13:08 +03:00
evgeny 8023f6ee70 stcp: separate v4/v6 sockets, protocol byte in chat addr format, error on bind fail
- stcp_server_create: support AF_INET and AF_INET6 V6ONLY=1 (no dual-stack)
- Add listen_family to stcp_link_config
- init_sockets: return error (not partial) on any bind failure
- add chat_core_sync_my_addresses() call at chatgui startup
- collect_my_addrs: add proto byte (1=UDP, 2=TCP) to binary format
- chat_sync.c JOIN/WELCOME/PEER_UPSERT: read proto byte, write protocol column
- member_sync.c: read proto byte, dynamic protocol bind
- topo_node.c: debug logs for nodeinfo changed detection
- node_config.cpp: type=public for all servers
2026-08-03 20:35:46 +03:00
evgeny e1e54df4f4 fix: invite не затирает имя узла, имя читается из member_sync peers.userinfo
- conn_mgr_core.c: убрана запись node_name из cm_handle_invite_info_resp
- topo_node_sqlite.c: name в nodes не перезаписывается пустой строкой
- android_jni_bridge.c: bridge_get_node_name читает peers_<ch>.userinfo если nodes.name пуст
2026-08-03 18:50:11 +03:00
evgeny ddf696bcf6 refactor: TCP socket addresses — sync to chat DB, topo_node, config file single source
- chat: collect_my_addrs extends to TCP + IPv6, chat_core_sync_my_addresses writes TCP addrs
- topo_node: replace CFG_SERVER with TCP_SOCKET for TCP addresses, simplify NAT logic
- etcp_connections: add IPv6 interface addr resolution for TCP sockets
- Android: INI config file as single source of truth for servers, remove JSON/Gson
- Android: filter temporary IPv6 addresses, fix restart with socket cleanup
- instance_lite: sync addresses after socket creation, proper restart cleanup
2026-08-03 13:57:42 +03:00
evgeny 740ed2a93a fix: NCD UAF, conn_mgr invite protocol, node_id derivation, link reuse
node_conn_direct:
- ncd_init_cb: skip UP when links_up==0, ncd_up_cb delivers when link comes up
- close/force_close: handle_count safety checks, callback clearing
- open/open_node case 2: create links on reused dead conn
- ncd_create_links: skip duplicate link on same conn (stale->etcp==conn)
- set_callback: optional callback change on live handle

conn_mgr:
- cm_ncd_callback: DOWN only delivers event (handle stays alive)
- cm_ncd_callback: TIMEOUT force_close BEFORE cm_deliver_event
- cm_deliver_event: read handle fields to locals before callback (reentrany-safe)
- INVITE_INFO_REQ/RESP: add duplicate cmd byte for direct-send protocol
- cm_handle_invite_info_resp: search invite by group_id in channel conn_mgr

Android:
- node_id computed by sc_derive_node_id_from_pubkey in C, not Kotlin
- utun_bridge_join_channel: recompute node_id, log mismatch
- invite_link_c: compute nodeId from pubkey via sc_derive_node_id_from_pubkey
- NativeLib.deriveNodeId, InviteLink.kt uses C function
2026-08-03 09:51:06 +03:00
evgeny c2191aad3b chat: user-friendly INFO logs for invite/connect flow
- chat_sync.c: invite entry, JOINED/TIMEOUT messages
- conn_mgr_core.c: invite start (addrs+udp+tcp), NCD UP/DOWN/TIMEOUT,
  TCP connect/ready/close, membership check send/receive/confirm/deny,
  overall timeout
- node_conn_direct.c: NCD event names instead of numbers
2026-08-02 20:32:01 +03:00
evgeny 50156e12af android: member list screen + link status dots, ipv6 autoconnect fixes
Android chatgui:
- New MemberListScreen with member list, detail card, edit dialog
- Link status dots (green/red/grey) per address in detail card (1s poll)
- Self-node name coloured green
- ChatScreen: people icon in TopAppBar

IPv6 autoconnect fixes (3 bugs in conn_mgr/topo):
- topo_node_sqlite_node_load: load v6 addresses from DB
- topo_node_registry_store: merge v6_addrs into existing node
- cm_sock_v6_classify: wildcard [::] treated as CM_V6_DIR
- cm_add_v6_link: htons(port) was missing

Memory fixes:
- strdup→u_strdup in bridge (7 instances)
- u_realloc cap update before success check (3 instances)
- char sql[256]→[512] for truncated SELECT

Chat member hash now includes addresses for change detection

Linux chatgui:
- Link status dots in AccountList detail panel (1s poll)
- Self-node name in green in member list
2026-08-01 23:40:58 +03:00
evgeny 6719a1e23c IPv6: STCP dual-stack + Android config + invite fixes
- stcp_server: dual-stack (AF_INET6 + IPV6_V6ONLY=0) instead of IPv4-only
- stcp_client: getaddrinfo(AF_UNSPEC) instead of inet_pton(AF_INET)
- stcp_link: detect address family from remote_addr->ss_family
- platform_compat.h: add <netdb.h> for getaddrinfo on POSIX
- ConfigProvider.kt: add [server:main6] addr=[::]:PORT for Android
- invite_link_c.c: invite_serialize_addrs handles IPv6 (was IPv4-only)
2026-08-01 20:19:00 +03:00
evgeny 51d6c782ac voice playback: miniaudio cursor ground truth + EMA smooth decay
- SoundManager: add currentPcmCursor()/isCurrentPcmPlaying() via ma_sound_get_cursor_in_pcm_frames / ma_sound_is_playing
- messagedelegate: replace wall-clock position tracking with real miniaudio cursor, remove 200ms hardcoded delay
- messagedelegate: end-of-playback detection via isCurrentPcmPlaying() instead of wall-clock comparison (was killing audio ~200ms early)
- messagedelegate: EMA smoothing of displayFrac/displayElapsed (0.15 factor, 16ms timer)
- messagedelegate: timer continues after audio stops to animate EMA decay to 0 (not mouse-move dependent)
- media_delivery: download progress callback, Ed25519 sig verify, author fallback, content hash check
- conn_mgr: already-connected returns OK with callback instead of error
- chatgui: voice download-pending auto-play, file download progress bar, media bar colors
2026-07-31 00:17:03 +03:00
evgeny df13b16fad docs: AGENTS.md chat module descriptions, improved ETCP debug logging, android AGENTS update 2026-07-30 12:20:04 +03:00
evgeny a3b00984ed fix: stable ETCP reconnect (no REINIT loop), own voice playback, peersOnline via conn state, UDP log diagnostics 2026-07-30 01:21:40 +03:00
evgeny 70797f921e feature: media auto-download + manual download + UI download state indication (Android + desktop)
C-layer:
- chat_event.h: CHAT_EVT_ATTACHMENT_DOWNLOADED (14)
- chat_msg.c: fixed md_auto_download (non-NULL done_cb, correct media_base from db_path, use chat_setting for autoload checks)
- chat_msg.c: added chat_core_attachment_download + trampoline for manual download
- chat_core.h: exported attachment_dl_req + trampoline

Android (Kotlin + JNI):
- Message: downloadState field (parse from localAttrs)
- ChatRepository: parse localAttrs for download state
- NativeLib: attachmentDownload() JNI
- MessageBubble: FileBubble with download status icon + tap-to-download
- ChatViewModel: downloadAttachment() + CHAT_EVT_ATTACHMENT_DOWNLOADED handler
- ChatScreen: wire onDownloadFile handler
- android_jni_bridge: utun_bridge_attachment_download + JNI + localAttrs in JSON

Desktop (Qt/C++):
- messagedelegate.h: MsgFileDownloadStateRole, MsgChannelIdRole
- messagelist.cpp: parse localAttrs for file download state
- messagedelegate.cpp: FileBubble with download icon/text + click handler via chat_core_attachment_download_trampoline
- gui_bridge.h: GUI_EVT_ATTACHMENT_DOWNLOADED (14) + callback typedef
- gui_bridge_impl.cpp: event dispatch + setter
- mainwindow.cpp: callback registration -> refresh message list
2026-07-30 00:01:38 +03:00
evgeny b8920cf483 chat_settings: [chat] config section + live sync + fix test segfault
- New chat_setting.c/h: unified chat settings API (int/str getters)
- Config parser supports [chat] section, calls chat_setting_set per key
- chat_core exposes chat_core_set_setting / trampoline for GUI bridge
- chatgui: AudioDevice + Storage pages persist to [chat] config + live apply
- chatgui-android: ConfigProvider, NativeLib, headless handle chat settings
- MessageList: 200 msg limit, fixed saveChannelState via trampoline
- test_media_delivery_full: fix segfault at shutdown (mon() timer vs freed conns)
2026-07-29 21:18:49 +03:00
evgeny 65dbfa6813 feat: media_async + media_index modules, media_files table, tests
- media_async: thread-per-task async engine with primitives (sha256, sign, copy, uuid, file_size)
- media_index: media_files DB table with register_async for indexing chunks
- chat_msg: async media submission via register_async, simplified chat_msg_submit fields
- voice_recorder/attachment_sender: write single file (no chunk splitting on caller side)
- inputbar/messagelist: updated to new simplified submit interface
- test_media_async: 4-thread stress test (200K ops / 2 sec, 0 errors)
- test_media_index: 9 cases covering commit edge cases + async flow
- build: Makefile.am, CMakeLists.txt, utun_sources.cmake updated
2026-07-29 14:15:10 +03:00
evgeny b7988f0ef8 refactor: single source of truth — PlaybackState in ViewModel
VoiceBubble is now a pure renderer — no local state, no LaunchedEffect, no timer.
All state from ViewModel.playbackState StateFlow:
  - playing/paused/elapsedMs/progress/filePath — one atomic snapshot
  - isActive = playbackState.filePath == message.filePath — only active clip shows cursor
  - onProgress callback from AudioRecorderManager feeds real elapsed into ViewModel
  - ViewModel timer (80ms) updates progress between onProgress callbacks

Removed: VoiceBubble's playing/paused/seekVersion/PlayState/onDone/onPaused.
Removed: AudioRecorderManager's isPaused/elapsedMs/pausedElapsedMs.
2026-07-29 02:29:53 +03:00
evgeny 9742772880 fix: monotonic playGeneration instead of re-settable stopped flag
stopped flag was set before join() but cleared after, causing race:
if thread was still blocked in decodeRead when stopped was reset,
onComplete fired anyway. Monotonic playGeneration ++ never decreases,
thread captures myGen at start, compares after completion —
correctly skipped if generation changed (another stop/seek started).
2026-07-29 02:20:25 +03:00
evgeny e4af56856c fix: atomic PlayState replaces non-atomic seekVersion+elapsed+progress
PlayState data class bundles playId, elapsedMs, progress into one atomic snapshot.
LaunchedEffect keyed on playId — when it changes (seek/resume), the timer reads
elapsedMs and progress from the SAME snapshot, eliminating the race between
seekVersion++ and elapsedMs=seekMs.
2026-07-29 02:13:47 +03:00
evgeny 1481d1244b fix: seekVersion forces LaunchedEffect restart on seek/resume
When playing=true and seek happens, LaunchedEffect(playing) key didn't change,
so the elapsed timer kept using the old start time. Added seekVersion counter
as second key — increments on waveform tap and play/resume click, forcing
the timer coroutine to restart with the new elapsedMs position.
2026-07-29 02:10:51 +03:00
evgeny 0afc74e567 fix: stop/pause no longer fires onComplete from killed thread
When stopPlayback/pausePlayback kills the playback thread, the thread's
onComplete callback was still firing, resetting UI state (playing=false,
elapsed=0). Added @Volatile 'stopped' flag — onComplete only fires on
natural completion, not on external stop/pause.
2026-07-29 02:08:51 +03:00
evgeny 2edd9bcc06 cleanup: remove dead currentPlayPath, fix seekVoice _currentPlayingPath reset
- AudioRecorderManager: removed unused currentPlayPath (duplicate of ViewModel state)
- ChatViewModel: seekVoice now resets _currentPlayingPath on complete (prevents
  pause-on-already-ended after seek finishes)
2026-07-29 02:03:09 +03:00
evgeny 2e8aa89397 voice: fix waveform — pointerInput on Box, cursor + white bars stay on pause
- Canvas pointerInput moved to wrapping Box (state updates trigger recomposition)
- showCursor = playing || paused (cursor and white bars visible on pause)
- elapsed/dur stays visible on pause too
2026-07-29 02:00:43 +03:00
evgeny 6980f34df9 voice: real pause (saves position), waveform tap seek, resume from position
- AudioRecorderManager: playFromPosition(startMs), pausePlayback, elapsedMs tracking
  playVoiceFile now delegates to playFromPosition. fade skipped PCM frames on seek.
- ViewModel: playVoiceMessage with pause toggle, seekVoice method
- VoiceBubble: click tap on waveform → seek, pause state (⏸ keeps cursor visible)
  Webform canvas with detectTapGestures for seek input
2026-07-29 01:57:31 +03:00
evgeny 887bbdbf84 voice: play/pause toggle, stop previous on new track
- ViewModel: _currentPlayingPath tracks active track. Tap same → stop.
  Tap different → stop old, play new. onComplete clears currentPlayingPath.
- AudioRecorderManager: stopPlayback() already called at start (existing)
- VoiceBubble: simplified click, always sends to ViewModel for state management
2026-07-29 01:51:25 +03:00
evgeny b8e15ad1f2 voice: played bars white, yellow cursor line on waveform 2026-07-29 01:48:16 +03:00
evgeny ee8167793c voice: waveform idle bars lighter (#CCD4 80% — near white) 2026-07-29 01:45:03 +03:00
evgeny 109326717e voice: play button white circle, black icon 2026-07-29 01:44:26 +03:00
evgeny f745fb25e8 android: lighten waveform bars (idle: #A0A0B0 70%, playing unplayed: 35% alpha) 2026-07-29 01:43:55 +03:00
evgeny b65a091b00 voice: base64 compact waveform (100 bytes), progress bar, play/pause auto-reset
- voice_recorder: 100-char base64 (A-Za-z0-9+/) log-scale waveform, ~120 bytes total vs ~600 CSV
- ChatRepository: parse base64 'wf=<100 chars>;dur=NNNN;' → normalized 0..1 List<Float>
  CSV fallback for old messages (length < 50 or contains comma)
- VoiceBubble: progress bar (blue played / gray unplayed), ▶/⏸ toggle, elapsed timer
- onComplete callback wired through ViewModel → AudioRecorderManager → VoiceBubble auto-reset
2026-07-29 01:36:43 +03:00
evgeny 491fa84db8 voice: fix waveform display + text buffer, 75% width, play/pause state
- bridge: txt buffer 256→2048, limit 250→2000 (voice d-field can be ~900 bytes)
- VoiceBubble: Column layout — play+waveform on top, duration|timestamp on bottom
- Play/pause toggle with ⏸/▶ icons
- Voice messages use 75% screen width max (300dp for others)
2026-07-29 01:21:35 +03:00
evgeny 94a537ebdd voice: waveform recording + display, playback wiring
- voice_recorder: waveform_levels[100] accumulated during feed, normalized to 100 values in stop()
  stored as 'wf=...;dur=...;' in chat_msg_submit data
- bridge: filePath in get_messages_json from local_attrs.fp, g_db_path moved global
- Message model: waveform: List<Float>, voiceDurationMs: Int, filePath: String
- VoiceBubble: Canvas waveform (100 bars, log scale), play button, duration
- ChatViewModel.playVoiceMessage → AudioRecorderManager.playVoiceFile
2026-07-29 01:15:57 +03:00
evgeny 39d6f19d97 fix: chat_msg media_dir path — strip filename from g_cc.db_path
g_cc.db_path is '/data/.../files/chats.db' (file path), but media blocks
are written to '/data/.../files/media/<channel>/'. chat_msg was concatenating
'chats.db/media/...' — now strips the database filename, using only the
directory portion for the media path.
2026-07-29 01:04:27 +03:00
evgeny b11e705f7a android: input bar — single right icon (PTT when empty, Send when text) 2026-07-29 00:58:26 +03:00
evgeny b90728be3c android: round send button with \u27A4 icon (matching desktop chatgui) 2026-07-29 00:55:39 +03:00
evgeny 9fca8d5e1f android: fix voice file creation (mkdir errno logging), PTT while(true)
- voice_recorder: proper mkdir with errno/EEXIST check, errno logged on fopen failure
- MessageBubble: pointerInput wrapped in while(true) so PTT works after first gesture
2026-07-29 00:53:23 +03:00
evgeny 00afc5c80a android: fix PTT — one Box across all states, release now works; 1s min duration
- PTT Box: same composable identity in idle/recording states (just changes bg color)
  Gesture coroutine survives recomposition → onPttReleased() fires on finger lift.
  When locked, Box is replaced by cancel/send IconButtons.
- Min voice duration: 500→1000ms (C and ViewModel)
2026-07-29 00:32:10 +03:00
evgeny 90de87b13d move audio_compressor from libutun_lite/ to lib/ — shared library
- audio_compressor.h/c moved to lib/ (alongside opus_codec, mem, debug_config)
- lib/Makefile.am: added to libuasync_a_SOURCES
- utun_sources.cmake: removed from _cfg_src (GLOB from lib/ picks it up)
- tests/Makefile.am: test_audio_compressor added to check_PROGRAMS
- test includes simplified (debug_config.h, mem.h — lib/ in include path)
2026-07-29 00:26:45 +03:00
evgeny d22eac55f7 fix audio_compressor: flat array instead of ring buffer, +unit test (411 asserts)
Ring buffer (pending_head/pending_tail/cap) replaced with simple flat array (pending_count/cap).
u_realloc copies linearly but ring buffer needs memmove for wrap-around — mismatch caused
SIGSEGV write to 0x10 on ARM64. Flat array: append to end, process from [0], memmove down.
Performance unchanged (max ~15 blocks).

Tests: create/destroy, passthrough disabled, basic push+flush, silence, reset, stress (200 cycles)
2026-07-29 00:11:29 +03:00
evgeny fc2adcf81d android: fix voice recording — init g_rec, move includes, check startRecording result
- voice_recorder_init(db_path) in utun_bridge_init (was never called, g_rec stayed NULL)
- voice_recorder_deinit() in utun_bridge_destroy
- ViewModel: set _isRecording only after startRecording() succeeds
2026-07-28 23:52:33 +03:00
evgeny 50214d50c1 android: fix PTT button — Box+pointerInput(hasMicPerm) instead of IconButton+onClick conflict 2026-07-28 23:35:53 +03:00
evgeny 1f8f41bda7 android: tabbed settings (Settings/Audio) with mic test, codec preset, compressor sliders
- voice_recorder: compressor config API (max_gain, lookback, lookahead, rise_rate, target_level)
- bridge/JNI: compressor_config export
- AudioRecorderManager: startMicTest/stopMicTest for level preview
- SettingsScreen: TabRow with Settings + Audio tabs
- Audio tab: mic test with live level bar, Opus preset selector, 5 compressor sliders matching desktop
2026-07-28 23:33:19 +03:00
evgeny 9b6dd6476c android: PTT swipe-to-lock, recording overlay with level bar, cancel/send buttons
- voice_recorder: peak_level tracking in feed(), voice_recorder_get_peak_level()
- bridge/JNI: voice_get_peak_level API
- ViewModel: isRecordingLocked, recordingDurationMs with 80ms timer, lock/send/cancel
- InputBar: 3 states (idle/holding/locked), custom pointerInput gesture with swipe-up lock
- ChatScreen: poll peak level 12x/sec during recording
2026-07-28 23:28:19 +03:00
evgeny 0f1e6e0006 android: fix NDK build — add Opus sources, fix Kotlin composable context, fix bridge db_path param 2026-07-28 23:20:19 +03:00
evgeny 5b6979da27 android: voice messages recording + playback, file attachments
- C99 port of AudioCompressor (libutun_lite/audio_compressor.h/c)
- Voice recorder: PCM buffer + compressor + Opus encode + block splitting + uasync send
- Attachment sender: file splitting into blocks + uasync send
- Bridge API + JNI: voice start/feed/stop/cancel, attachment send, Opus decode for playback
- Kotlin: AudioRecorderManager (AudioRecord + JNI feed), AudioPlayer (AudioTrack + JNI decode)
- UI: PTT button with press-to-talk, attach button with file picker, recording overlay
- Message display: voice messages (play button), file attachments (icon + name)
- Settings: Opus preset selector (Low/Standard/High), compressor toggle
- AndroidManifest: RECORD_AUDIO, storage permissions
2026-07-28 23:15:59 +03:00
evgeny e751e6ee69 android: voice messages + attachments architecture plan, build fixes 2026-07-28 23:07:23 +03:00
evgeny f8d7c21a79 Android: unify DB access via JNI/JSON, fix instance lifecycle, add restart + health check
- ChatRepository: remove SQLiteDatabase (READONLY 2nd handle), use JNI→shared g_cc.db (FULLMUTEX, like desktop)
- instance_lite: simplify start/stop lifecycle — start no longer waits for init, stop waits for thread exit with timeout, add internal restart (no thread kill)
- Restart trampoline: reinit UDP log target on config change
- Health check: ping/uasync_post round-trip every 3s, red dot on stall
- Settings: Restart instance button
- Fix: g_inst check instead of g_running for is_running()
- Fix: SharedFlow replay=1 + proactive DB_READY check in ViewModel
2026-07-28 21:13:11 +03:00
evgeny 03b34e9580 android: fix QR join flow + cleanup log levels + remove log artifacts
- MainActivity: call NativeLib.joinChannel directly (bypass ViewModel null repo)
- LogManager: add Log.d for Kotlin logs visibility in logcat
- ChatViewModel/NativeLib: add diagnostic logs for join path
- chat_sync: JNI-safe logs via __android_log_print, add error on !g_cs
- etcp_debug: DUMP INFO→DEBUG (remove 99.8% packet spam)
- secure_channel/etcp_connections/topo_node: keepalive PING/PONG spam INFO→DEBUG
- .gitignore: logreceiver output + build artifacts
- Remove tracked log files and build dir from repo
2026-07-28 18:11:11 +03:00