Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions android/app/src/main/res/values/colors.xml
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@
<resources>
<color name="ic_launcher_background">#121212</color>
<color name="ic_launcher_background_og">#FF5500</color>
<color name="ic_launcher_background_chrome">#C0C0C0</color>
<color name="ic_launcher_background_rose_gold">#B76E79</color>
<color name="ic_launcher_background_chrome">#78909C</color>
<color name="ic_launcher_background_rose_gold">#C5848F</color>
<color name="ic_launcher_background_silver">#A8A9AD</color>
<color name="ic_launcher_background_soft_purple">#121212</color>
<color name="ic_launcher_background_hot_pink">#121212</color>
<color name="ic_launcher_background_tie_dye">#121212</color>
<color name="ic_launcher_background_leopard">#121212</color>
<color name="ic_launcher_background_soft_purple">#A78BFA</color>
<color name="ic_launcher_background_hot_pink">#FF3EA5</color>
<color name="ic_launcher_background_tie_dye">#4ECDC4</color>
<color name="ic_launcher_background_leopard">#8B5E3C</color>
</resources>
6 changes: 2 additions & 4 deletions lib/core/routing/app_router.dart
Original file line number Diff line number Diff line change
Expand Up @@ -257,10 +257,8 @@ final routerProvider = Provider<GoRouter>((ref) {
orElse: () => null,
);

return ChatScreen(
conv: conv,
newParticipantId: conv == null ? chatId : null,
);
if (conv != null) return ChatScreen(conv: conv);
return ChatScreen(convId: chatId);
},
routes: [
GoRoute(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ class DatasourceImplement implements DatasourceInterface {
DatasourceImplement({required this.dio});

@override
/// Fetches all conversations for the current user from `GET /messages/conversations`.
///
/// Expects a JSON envelope `{ data: { items: [...] } }`.
Future<List<ConversationModel>> getConversations() async {
final response = await dio.get(ApiEndPoints.getConversations);
if (response.data is! Map<String, dynamic>) {
Expand All @@ -34,6 +37,11 @@ class DatasourceImplement implements DatasourceInterface {
}

@override
/// Fetches a page of messages for [conversationId] at [offset].
///
/// Handles two response shapes: `data` as a Map containing `messages` + `pagination`,
/// or `data` as a raw List. Falls back to `rawMessages.length` when `total` is absent.
/// Returns a tuple of the parsed [MessageModel] list and the total message count.
Future<(List<MessageModel>, int)> getMessages({
required String conversationId,
int offset = 0,
Expand Down Expand Up @@ -74,6 +82,9 @@ class DatasourceImplement implements DatasourceInterface {
}

@override
/// Sends a message to [conversationId] via `POST /messages/conversations/{id}/messages`.
///
/// Serialises [requestContent] to JSON and returns the created [MessageModel].
Future<MessageModel> sendMessage({
required String conversationId,
required SentMessageRequestModel requestContent,
Expand All @@ -86,6 +97,11 @@ class DatasourceImplement implements DatasourceInterface {
}

@override
/// Starts a new conversation with [participantId] via `POST /messages/new`.
///
/// Optionally attaches a text [body] or a music resource ([trackId] or [playlistId]).
/// If the response does not contain a `conversation` key, falls back to fetching
/// the conversation list and finding the one matching [participantId].
Future<ConversationModel> newConversation({
required String participantId,
String? body,
Expand Down Expand Up @@ -114,6 +130,10 @@ class DatasourceImplement implements DatasourceInterface {
}

@override
/// Ensures a conversation with [participantId] exists via `POST /messages/conversations/ensure`.
///
/// Creates the conversation if it doesn't exist; returns the existing one otherwise.
/// Used when navigating to a chat screen without sending a message.
Future<ConversationModel> ensureConversation({
required String participantId,
}) async {
Expand All @@ -127,22 +147,29 @@ class DatasourceImplement implements DatasourceInterface {
}

@override
/// Returns the total number of unread messages via `GET /messages/unread-count`.
Future<int> getUnreadCount() async {
final response = await dio.get(ApiEndPoints.getUnreadCount);
return response.data['data']['unread_count'] as int;
}

@override
/// Blocks [userId] via `POST /users/{userId}/block`.
Future<void> blockUser({required String userId}) async {
await dio.post(ApiEndPoints.blockUser(userId));
}

@override
/// Unblocks [userId] via `DELETE /users/{userId}/block`.
Future<void> unBlockUser({required String userId}) async {
await dio.delete(ApiEndPoints.unBlockUser(userId));
}

@override
/// Marks [messageId] as read via `PATCH /messages/conversations/{convId}/messages/{msgId}/read`.
///
/// A 409 response means the message is already read and is silently swallowed.
/// All other [DioException]s are rethrown.
Future<void> markMessagesAsRead({
required String conversationId,
required String messageId,
Expand All @@ -159,6 +186,7 @@ class DatasourceImplement implements DatasourceInterface {
}

@override
/// Returns the list of users followed by the current user via `GET /users/me/following`.
Future<List<PotentialConversationModel>> getFollowings(String myId) async {
try {
final url = ApiEndPoints.getFollowings();
Expand All @@ -180,6 +208,7 @@ class DatasourceImplement implements DatasourceInterface {
}

@override
/// Searches for users matching [query] via `GET /search?q={query}&type=users`.
Future<List<PotentialConversationModel>> getSearchedUsers(
String query,
) async {
Expand All @@ -202,6 +231,9 @@ class DatasourceImplement implements DatasourceInterface {
}

@override
/// Returns `true` if the current user has blocked [participantId].
///
/// Reads the `is_blocking` field from `GET /users/{id}/follow-status`.
Future<bool> isBlocked(String participantId) async {
final response = await dio.get(ApiEndPoints.isBlocked(participantId));

Expand All @@ -217,6 +249,10 @@ class DatasourceImplement implements DatasourceInterface {
}

@override
/// Returns `true` if [participantId] has blocked the current user.
///
/// Reads the `is_blocked_by` field from the same `GET /users/{id}/follow-status`
/// endpoint used by [isBlocked].
Future<bool> isBlockedBy(String participantId) async {
final response = await dio.get(
ApiEndPoints.isBlocked(participantId), //same endpoint as isBlocked
Expand All @@ -234,6 +270,12 @@ class DatasourceImplement implements DatasourceInterface {
}

@override
/// Returns the current user's liked embeds filtered by [embedType].
///
/// - `track`: fetches liked tracks from `GET /me/liked-tracks`.
/// - `playlist`: merges liked playlists and user-created playlists,
/// deduplicating by ID so the same playlist never appears twice.
/// - `album`: fetches liked albums from `GET /me/liked-albums`.
Future<List<SharedEmbedModel>> getEmbeds(
String userId,
String embedType,
Expand Down Expand Up @@ -303,6 +345,10 @@ class DatasourceImplement implements DatasourceInterface {
}

@override
/// Fetches track metadata for [trackId] from `GET /tracks/{trackId}`.
///
/// Tries `artist_name`, then `artist`, then `artists` for the artist field
/// to handle inconsistencies across different track response shapes.
Future<SharedEmbedModel> getTrackDetails(String trackId) async {
final response = await dio.get(ApiEndPoints.getTrackDetails(trackId));
final data = response.data['data'];
Expand All @@ -316,6 +362,11 @@ class DatasourceImplement implements DatasourceInterface {
}

@override
/// Fetches playlist or album metadata from `GET /playlists/{playlistId}`.
///
/// [embedType] is passed through to [SharedEmbedModel] to distinguish
/// between `playlist` and `album` at the domain level.
/// Falls back to [playlistId] when `playlist_id` is absent from the response.
Future<SharedEmbedModel> getPlaylistDetails(
String playlistId,
String embedType,
Expand Down
5 changes: 5 additions & 0 deletions lib/features/messaging/data/models/shared_embed_model.dart
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,11 @@ class SharedEmbedModel extends SharedEmbed {
super.thumbnailUrl,
});

/// Deserialises a [SharedEmbedModel] from an already-normalised [json] map.
///
/// This factory is used internally after the datasource has normalised
/// field names (e.g. mapping `track_id` → `embedId`). It is NOT called
/// directly on raw API responses.
factory SharedEmbedModel.fromJson(Map<String, dynamic> json) {
return SharedEmbedModel(
embedId: json['embedId'] as String,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,15 @@ class RepositoryImplement implements MessagingRepository {
RepositoryImplement({required this.dataSource});

@override
/// Fetches all active conversations for the current user.
Future<List<Conversation>> getConversations() {
return dataSource.getConversations();
}

@override
/// Fetches a paginated page of messages for [conversationId] starting at [offset].
///
/// Returns a tuple of the message list and the total message count.
Future<(List<Message>, int)> getMessages(
String conversationId, {
int offset = 0,
Expand All @@ -33,6 +37,9 @@ class RepositoryImplement implements MessagingRepository {
}

@override
/// Starts a new conversation with [participantId].
///
/// Passes an optional [body] text and/or a [trackId] or [playlistId] embed.
Future<Conversation> startConversation(
String participantId,
String? body,
Expand All @@ -48,6 +55,10 @@ class RepositoryImplement implements MessagingRepository {
}

@override
/// Sends a message within an existing [conversationId].
///
/// Builds a [SentMessageRequestModel] from the provided fields before
/// delegating to the datasource.
Future<Message> sendMessage(
String conversationId,
String? body,
Expand All @@ -66,16 +77,19 @@ class RepositoryImplement implements MessagingRepository {
}

@override
/// Blocks the user identified by [participantId].
Future<void> blockUser(String participantId) {
return dataSource.blockUser(userId: participantId);
}

@override
/// Unblocks the user identified by [participantId].
Future<void> unBlockUser(String participantId) {
return dataSource.unBlockUser(userId: participantId);
}

@override
/// Marks [messageId] as read within [conversationId].
Future<void> markMessageAsRead(String messageId, String conversationId) {
return dataSource.markMessagesAsRead(
conversationId: conversationId,
Expand All @@ -84,46 +98,59 @@ class RepositoryImplement implements MessagingRepository {
}

@override
/// Returns the total count of unread messages across all conversations.
Future<int> getUnReadCount() {
return dataSource.getUnreadCount();
}

@override
/// Returns the list of users followed by the current user ([myId]).
Future<List<PotentialConversation>> getFollowings(String myId) {
return dataSource.getFollowings(myId);
}

@override
/// Searches for users matching [query].
Future<List<PotentialConversation>> getSearchedUsers(String query) {
return dataSource.getSearchedUsers(query);
}

@override
/// Returns `true` if the current user has blocked [participantId].
Future<bool> isBlocked(String participantId) {
return dataSource.isBlocked(participantId);
}

@override
/// Returns `true` if [participantId] has blocked the current user.
Future<bool> isBlockedBy(String participantId) {
return dataSource.isBlockedBy(participantId);
}

@override
/// Returns the current user's liked embeds filtered by [embedType].
///
/// [embedType] is one of `track`, `playlist`, or `album`.
Future<List<SharedEmbed>> getLikedEmbeds(String userId, String embedType) {
return dataSource.getEmbeds(userId, embedType);
}

@override
/// Returns track metadata for [trackId] as a [SharedEmbed].
Future<SharedEmbed> getTrack(String trackId) {
return dataSource.getTrackDetails(trackId);
}

@override
/// Returns playlist or album metadata for [playlistId] as a [SharedEmbed].
///
/// [embedType] distinguishes between `playlist` and `album`.
Future<SharedEmbed> getPlaylist(String playlistId, String embedType) {
return dataSource.getPlaylistDetails(playlistId, embedType);
}

@override
/// Ensures a conversation with [participantId] exists, creating one if needed.
Future<Conversation> ensureConversation(String participantId) {
return dataSource.ensureConversation(participantId: participantId);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,17 @@
import 'package:rythmify/features/messaging/domain/entities/conversation.dart';
import 'package:rythmify/features/messaging/domain/repositories/messaging_repository.dart';

/// Ensures a conversation exists with [participantId] without sending a message.
///
/// Used when navigating directly to a chat screen — guarantees a conversation
/// record exists on the backend before any messages are loaded or sent.
/// If the conversation already exists, the server returns the existing one.
class EnsureConversationUsecase {
final MessagingRepository repo;

EnsureConversationUsecase({required this.repo});

/// Returns the existing or newly created [Conversation] with [participantId].
Future<Conversation> call(String participantId) {
return repo.ensureConversation(participantId);
}
Expand Down
Loading
Loading