Skip to content

Commit 2e7d020

Browse files
committed
feat: toastService - notification in windows
feat: ticks for sent messages (also want response=true) feat: faster BLE connection fix: chat scrolling feat: text and channel display more like modern messengers
1 parent 8d46c5e commit 2e7d020

20 files changed

Lines changed: 918 additions & 163 deletions

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,3 +71,7 @@ TILE_AUTH_SERVER.md
7171
tile-keys/
7272
*.tileauth.key
7373
secret.key
74+
75+
# Legal drafts (Impressum, Datenschutz) — contain personal/operator data,
76+
# kept out of the public repo until finalized.
77+
legal/

CHANGELOG.md

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,41 @@ und dieses Projekt folgt [Semantic Versioning](https://semver.org/lang/de/).
1111

1212
---
1313

14+
## [1.7.0] - 2026-09-10
15+
16+
### ✨ Hinzugefügt
17+
18+
#### 💬 Nachrichten-Übersicht mit Seitenleiste (WhatsApp-Stil)
19+
- Neue **Seitenleiste** im Nachrichten-Tab: links die Auswahl (Kanäle **und** Direktnachrichten), rechts der Chat. Das bewährte **Kontextmenü** und die farbigen Bubbles bleiben unverändert — die **Node-Farbe** wird weiterhin je Bubble berücksichtigt.
20+
- **Ungelesen-Badge** (grüne Blase mit Zähler) an Kanälen und DMs, die nicht gerade geöffnet sind.
21+
22+
#### ✅ Zustellstatus & erneutes Senden
23+
- Gesendete Nachrichten zeigen jetzt **Statushaken**: … (wird gesendet) → ✓ (gesendet) → ✓✓ (zugestellt, grün) bzw. ⚠ (fehlgeschlagen, rot). Basiert auf `want_ack` und der Routing-Bestätigung der Firmware (Android-Parität).
24+
- **Erneut-senden-Button** (↻) an fehlgeschlagenen Nachrichten.
25+
26+
#### 🔔 Direktnachrichten wahlweise inline oder im Fenster
27+
- In den Einstellungen umschaltbar: DMs **wie bei WhatsApp direkt im Client** (inline in der Übersicht) **oder** im separaten Fenster (Standard bleibt Fenster).
28+
29+
#### 🔔 Toast-Benachrichtigungen
30+
- Optionale **Windows-Toast-Benachrichtigungen** bei neuen Nachrichten (in den Einstellungen aktivierbar), nur wenn das Fenster nicht im Vordergrund ist.
31+
32+
#### 🚀 Verbindung
33+
- **BLE-Durchsatz optimiert**: bevorzugte Verbindungsparameter (`ThroughputOptimized`) werden angefordert — spürbar schnellerer Datenaustausch über Bluetooth.
34+
- **BLE-Reconnect** stabilisiert: GATT-Session/-Service und Verbindungsparameter werden beim Trennen sauber freigegeben, sodass ein erneutes Verbinden zuverlässig funktioniert.
35+
36+
> ℹ️ Diese Version hebt das Windows-SDK-Ziel auf `10.0.22621` an (für die BLE-Durchsatz-API). Mindest-Windows-Version bleibt unverändert.
37+
38+
### 🐛 Behoben
39+
- **Toast-Spam bei verschlüsselten Nachrichten**: Für unentschlüsselbare Nachrichten (Platzhalter „Verschlüsselte Nachricht – PSK erforderlich") wird **kein** Toast mehr angezeigt.
40+
- **„DM senden" springt jetzt in den Chat**: Der „DM senden"-Button an der Node-Liste respektiert nun den Inline/Fenster-Schalter — im Inline-Modus wird direkt in den Nachrichten-Tab gesprungen statt zusätzlich das DM-Fenster zu öffnen; im Fenster-Modus wird der passende Chat direkt geöffnet und aktiviert.
41+
- **„Info anfordern" im Chat**: Das Nachrichten-Kontextmenü (Rechtsklick auf eine Nachricht) hat jetzt das vollständige **„Info anfordern"**-Untermenü (Nutzer-Info, Position, Geräte-/Umwelt-/Luftqualitäts-/Power-Metriken, LocalStats, HostMetrics, PaxCounter) — zielt auf den Absender. Funktioniert auch bei **noch unbekannten Nodes** (nur die Node-ID ist nötig — genau so lernt man einen unbekannten Node kennen). Zusätzlich hat die **Chatliste selbst** (Direktnachrichten-Einträge) jetzt ein Rechtsklick-Menü mit „Node-Info", „Auf Karte zeigen" und „Info anfordern".
42+
- **Kanalauswahl-Dropdown neben dem Eingabefeld entfernt**: Der Sende-Kanal folgt jetzt der Auswahl in der Seitenleiste — das separate Dropdown war doppelt und ist weg. Mehr Platz fürs Eingabefeld.
43+
- **Chat startet unten (neueste Nachricht) statt oben**: Beim Start bzw. Kanal-/DM-Wechsel wird jetzt zuverlässig ans Ende gescrollt. Zuvor landete man im Back-Scroll ganz oben, weil das Nachladen älterer Nachrichten schon beim ersten Layout-Durchlauf (Scroll-Position 0) auslöste; das ist jetzt bis nach dem ersten Scroll-to-Bottom gesperrt.
44+
- **Chatliste und angezeigte Nachrichten stimmen beim Start überein**: Beim Start war in der Seitenleiste zwar ein Kanal (z. B. „short slow") ausgewählt, angezeigt wurden aber die Nachrichten eines anderen Kanals — erst ein erneuter Klick schaltete um. Ursache: der versteckte alte Kanal-Filter (ComboBox) überschrieb beim Befüllen die Auswahl der Seitenleiste mit „Alle Kanäle". Die Seitenleiste ist jetzt alleinige Filterquelle; der versteckte ComboBox kann sie nicht mehr überschreiben.
45+
- **Serielle Verbindung hängt nicht mehr beim Trennen**: Verband man sich versehentlich mit einem COM-Port, der kein Meshtastic-Node ist, und trennte sofort, konnte der Client einfrieren. Ursache: unendlicher Write-Timeout ließ Schreibvorgänge auf dem toten Port ewig blockieren, wodurch `SerialPort.Close()` beim Trennen deadlockte. Jetzt: finiter Write-Timeout (2 s), das Schließen läuft zeitlich begrenzt auf einem eigenen Thread, der Trenn-Vorgang ist reentranz-sicher, die Config-Wartephase bricht beim Trennen sofort ab, und die Oberfläche gibt nach spätestens 8 s garantiert wieder frei.
46+
47+
---
48+
1449
## [1.6.3] - 2026-08-31
1550

1651
### ✨ Hinzugefügt

MeshhessenClient.Tests/MeshhessenClient.Tests.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
<PropertyGroup>
44
<!-- Must match the main project's TFM to reference its (WPF) assembly.
55
The tested code (Services) touches no WPF types, so no UseWPF needed. -->
6-
<TargetFramework>net8.0-windows10.0.19041.0</TargetFramework>
6+
<TargetFramework>net8.0-windows10.0.22621.0</TargetFramework>
77
<ImplicitUsings>enable</ImplicitUsings>
88
<Nullable>enable</Nullable>
99
<IsPackable>false</IsPackable>

MeshhessenClient/MainWindow.Kiosk.cs

Lines changed: 50 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -511,8 +511,7 @@ private void LoadMessagesFromDbAsync()
511511
// Resolve stored "Kanal N" names against the current channel list, then rebuild.
512512
RefreshChannelNames();
513513
RebuildVisibleMessages();
514-
if (_messages.Count > 0)
515-
MessageListView.ScrollIntoView(_messages[^1]);
514+
ScrollMessagesToBottom();
516515
});
517516
}
518517
catch (Exception ex)
@@ -618,11 +617,27 @@ private void RebuildVisibleMessages()
618617

619618
private void MessageListView_ScrollChanged(object sender, ScrollChangedEventArgs e)
620619
{
620+
// Don't lazy-load until the initial scroll-to-bottom has happened — otherwise the
621+
// startup layout pass (VerticalOffset 0) would immediately drag us back to the top.
622+
if (!_initialScrollDone) return;
621623
// When scrolled to (near) the top, lazy-load older messages from DB
622624
if (e.VerticalOffset < 10 && _messageDbManager != null && _dbOldestTimestamp != long.MaxValue)
623625
LazyLoadOlderMessages();
624626
}
625627

628+
/// <summary>Scroll the chat to the newest message. Deferred to Background priority so the
629+
/// (virtualized) list has finished its layout pass before we scroll — otherwise the scroll
630+
/// is a no-op and we stay at the top. Also arms lazy-load-on-scroll-up.</summary>
631+
private void ScrollMessagesToBottom()
632+
{
633+
Dispatcher.BeginInvoke(new Action(() =>
634+
{
635+
if (_messages.Count > 0)
636+
MessageListView.ScrollIntoView(_messages[^1]);
637+
_initialScrollDone = true;
638+
}), System.Windows.Threading.DispatcherPriority.Background);
639+
}
640+
626641
private void ClearChannelMessages_Click(object sender, RoutedEventArgs e)
627642
{
628643
if (_messageDbManager == null) return;
@@ -724,25 +739,35 @@ private void ClearSelectedChannelDb_Click(object sender, RoutedEventArgs e)
724739

725740
private void UpdateMessageFilterComboBox()
726741
{
727-
// Speichere aktuelle Auswahl
728-
var selectedFilter = MessageChannelFilterComboBox.SelectedItem as ChannelInfo;
742+
// Populate the (hidden) ComboBox without letting its SelectionChanged clobber the
743+
// sidebar-driven _messageChannelFilter — see MessageChannelFilter_Changed.
744+
_suppressChannelFilterCombo = true;
745+
try
746+
{
747+
// Speichere aktuelle Auswahl
748+
var selectedFilter = MessageChannelFilterComboBox.SelectedItem as ChannelInfo;
729749

730-
// Erstelle Liste mit "Alle" Option
731-
var filterItems = new List<ChannelInfo>();
732-
filterItems.Add(new ChannelInfo { Index = 999, Name = Loc("StrAllChannels"), Role = "" });
733-
filterItems.AddRange(_channels);
750+
// Erstelle Liste mit "Alle" Option
751+
var filterItems = new List<ChannelInfo>();
752+
filterItems.Add(new ChannelInfo { Index = 999, Name = Loc("StrAllChannels"), Role = "" });
753+
filterItems.AddRange(_channels);
734754

735-
MessageChannelFilterComboBox.ItemsSource = filterItems;
755+
MessageChannelFilterComboBox.ItemsSource = filterItems;
736756

737-
// Stelle Auswahl wieder her oder wähle "Alle"
738-
if (selectedFilter != null)
739-
{
740-
var restored = filterItems.FirstOrDefault(c => c.Index == selectedFilter.Index);
741-
MessageChannelFilterComboBox.SelectedItem = restored ?? filterItems[0];
757+
// Stelle Auswahl wieder her oder wähle "Alle"
758+
if (selectedFilter != null)
759+
{
760+
var restored = filterItems.FirstOrDefault(c => c.Index == selectedFilter.Index);
761+
MessageChannelFilterComboBox.SelectedItem = restored ?? filterItems[0];
762+
}
763+
else
764+
{
765+
MessageChannelFilterComboBox.SelectedIndex = 0;
766+
}
742767
}
743-
else
768+
finally
744769
{
745-
MessageChannelFilterComboBox.SelectedIndex = 0;
770+
_suppressChannelFilterCombo = false;
746771
}
747772
}
748773

@@ -796,6 +821,9 @@ private void OpenDmWindow_Click(object sender, RoutedEventArgs e)
796821

797822
private void MessageChannelFilter_Changed(object sender, SelectionChangedEventArgs e)
798823
{
824+
// The ComboBox is hidden and only ever changes programmatically (via
825+
// UpdateMessageFilterComboBox). Ignore those so it can't overwrite the sidebar filter.
826+
if (_suppressChannelFilterCombo) return;
799827
try
800828
{
801829
_messageChannelFilter = MessageChannelFilterComboBox.SelectedItem as ChannelInfo;
@@ -824,14 +852,9 @@ private void MessageChannelFilter_Changed(object sender, SelectionChangedEventAr
824852
}
825853
}
826854

827-
// Sync send-channel dropdown to the selected filter channel
828-
// (skip "Alle Kanäle" sentinel with Index 999)
855+
// Send-channel now follows the sidebar selection (ActiveChannelComboBox removed).
829856
if (_messageChannelFilter != null && _messageChannelFilter.Index != 999)
830-
{
831-
var match = _channels.FirstOrDefault(c => c.Index == _messageChannelFilter.Index);
832-
if (match != null && !Equals(ActiveChannelComboBox.SelectedItem, match))
833-
ActiveChannelComboBox.SelectedItem = match;
834-
}
857+
_activeChannelIndex = (int)_messageChannelFilter.Index;
835858

836859
Services.Logger.WriteLine($"Message filter changed to: {_messageChannelFilter?.Name ?? "Alle"} ({_messages.Count}/{_allMessages.Count} messages)");
837860
}
@@ -860,15 +883,10 @@ private void SendDmToNode_Click(object sender, RoutedEventArgs e)
860883
return;
861884
}
862885

863-
// Öffne/Erstelle DM-Fenster
864-
if (_dmWindow == null)
865-
{
866-
_dmWindow = new DirectMessagesWindow(_protocolService, _myNodeId);
867-
_dmWindow.SetMessageDbManager(_messageDbManager);
868-
}
869-
870-
// Öffne Chat mit diesem Knoten
871-
_dmWindow.OpenChatWithNode(selectedNode.NodeId, selectedNode.Name);
886+
// Über OpenDmToNode leiten, damit der Inline/Fenster-Schalter aus den
887+
// Einstellungen respektiert wird (Inline: springt in den Nachrichten-Tab,
888+
// Fenster: öffnet das DM-Fenster und den passenden Chat).
889+
OpenDmToNode(selectedNode);
872890

873891
Services.Logger.WriteLine($"Opening DM chat with node: {selectedNode.Name} ({selectedNode.Id})");
874892
}

MeshhessenClient/MainWindow.Map.cs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1361,6 +1361,12 @@ private void SetMyPosition(double lat, double lon)
13611361

13621362
private void OpenDmToNode(NodeInfo node)
13631363
{
1364+
// Inline (WhatsApp-style) mode: open the DM in the main overview instead of a window.
1365+
if (DmInline)
1366+
{
1367+
OpenInlineDm(node.NodeId, node.Name, node.ColorHex);
1368+
return;
1369+
}
13641370
if (_dmWindow == null || !_dmWindow.IsVisible)
13651371
{
13661372
_dmWindow = new DirectMessagesWindow(_protocolService, _myNodeId);
@@ -1371,6 +1377,17 @@ private void OpenDmToNode(NodeInfo node)
13711377
_dmWindow.OpenChatWithNode(node.NodeId, node.Name, node.ColorHex);
13721378
}
13731379

1380+
/// <summary>Open (or create) an inline DM conversation and show it in the Messages overview tab.</summary>
1381+
private void OpenInlineDm(uint nodeId, string name, string color)
1382+
{
1383+
if (nodeId == 0 || nodeId == _myNodeId) return;
1384+
var dm = GetOrCreateDmItem(nodeId, name, color);
1385+
MainTabs.SelectedIndex = 0; // Messages tab
1386+
bool alreadyOpen = _currentDmPartner == nodeId;
1387+
DmList.SelectedItem = dm; // fires DmList_SelectionChanged → shows the chat
1388+
if (alreadyOpen) DmList_SelectionChanged(DmList, null!); // force reload if it was already selected
1389+
}
1390+
13741391
private void ShowNodeInfoDialog(NodeInfo node)
13751392
{
13761393
var win = new NodeInfoWindow(node) { Owner = this };

0 commit comments

Comments
 (0)