| .. | .. |
|---|
| 82 | 82 | final jsonStr = await file.readAsString(); |
|---|
| 83 | 83 | final List<dynamic> jsonList = jsonDecode(jsonStr) as List<dynamic>; |
|---|
| 84 | 84 | final allMessages = jsonList |
|---|
| 85 | | - .map((j) => Message.fromJson(j as Map<String, dynamic>)) |
|---|
| 85 | + .map((j) => _messageFromJson(j as Map<String, dynamic>)) |
|---|
| 86 | 86 | .where((m) => !m.isEmptyVoice) // Filter out voice msgs with no content |
|---|
| 87 | 87 | .toList(); |
|---|
| 88 | 88 | |
|---|
| .. | .. |
|---|
| 106 | 106 | final jsonStr = await file.readAsString(); |
|---|
| 107 | 107 | final List<dynamic> jsonList = jsonDecode(jsonStr) as List<dynamic>; |
|---|
| 108 | 108 | return jsonList |
|---|
| 109 | | - .map((j) => Message.fromJson(j as Map<String, dynamic>)) |
|---|
| 109 | + .map((j) => _messageFromJson(j as Map<String, dynamic>)) |
|---|
| 110 | 110 | .where((m) => !m.isEmptyVoice) |
|---|
| 111 | 111 | .toList(); |
|---|
| 112 | 112 | } catch (e) { |
|---|
| .. | .. |
|---|
| 114 | 114 | } |
|---|
| 115 | 115 | } |
|---|
| 116 | 116 | |
|---|
| 117 | + /// Deserialize a message from JSON, applying migration rules: |
|---|
| 118 | + /// - Voice messages without audioUri are downgraded to text (transcript only). |
|---|
| 119 | + /// This handles messages saved before a restart, where the temp audio file |
|---|
| 120 | + /// is no longer available. The transcript (content) is preserved. |
|---|
| 121 | + static Message _messageFromJson(Map<String, dynamic> json) { |
|---|
| 122 | + final raw = Message.fromJson(json); |
|---|
| 123 | + if (raw.type == MessageType.voice && |
|---|
| 124 | + (raw.audioUri == null || raw.audioUri!.isEmpty)) { |
|---|
| 125 | + // Downgrade to text so the bubble shows the transcript instead of a |
|---|
| 126 | + // broken play button. |
|---|
| 127 | + return Message( |
|---|
| 128 | + id: raw.id, |
|---|
| 129 | + role: raw.role, |
|---|
| 130 | + type: MessageType.text, |
|---|
| 131 | + content: raw.content, |
|---|
| 132 | + timestamp: raw.timestamp, |
|---|
| 133 | + status: raw.status, |
|---|
| 134 | + duration: raw.duration, |
|---|
| 135 | + ); |
|---|
| 136 | + } |
|---|
| 137 | + return raw; |
|---|
| 138 | + } |
|---|
| 139 | + |
|---|
| 117 | 140 | /// Delete stored messages for a session. |
|---|
| 118 | 141 | static Future<void> delete(String sessionId) async { |
|---|
| 119 | 142 | try { |
|---|