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
2 changes: 2 additions & 0 deletions openspec/changes/add-forum-filters/.openspec.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-07-09
42 changes: 42 additions & 0 deletions openspec/changes/add-forum-filters/design.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
## Context

The `ForumPostsListScreen` currently utilizes horizontal chips (`_CategoryChips`) to filter forum threads by category. As the need for more complex filtering arises, the UI will be updated to introduce sorting tabs (e.g., "Recent", "Most Liked", "Most Viewed") above the category chips. A new activity filter menu will be introduced via a Bottom Sheet, accessible via a filter icon on the main screen.

## Goals / Non-Goals

**Goals:**
- Introduce a sorting segmented control containing tabs for "Recent", "Most Liked", and "Most Viewed" on `ForumPostsListScreen`.
- Retain the horizontal category chips below the new sorting tabs.
- Introduce a Bottom Sheet (`ForumFilterBottomSheet`) accessible via a filter icon.
- Introduce an "Activity Filter" section within the Bottom Sheet with predefined options (e.g., Posted by me, Commented by me).
- Update the `GlobalForumFeed` Riverpod provider to accept and maintain the selected activity filter and sort order alongside the category filter.
- Update `ForumRepository` and the underlying data sources (`DataSource`) to pass the activity filter and sort parameters to the backend.

**Non-Goals:**
- Redesigning the entire forum feed layout or thread card UI.
- Modifying the underlying backend APIs (this design assumes the backend API for activity filtering is either already capable or will be updated independently to accept these parameters).

## Decisions

### 1. UI Architecture: Bottom Sheet
We decided to implement the activity filter menu as a Bottom Sheet (`AppBottomSheet`).
- **Rationale:** A bottom sheet provides a focused, modal interaction model for mobile screens, keeping the filter options easily accessible near the bottom of the screen.

### 2. State Management for Filters
The `ForumFilterBottomSheet` will trigger filter changes immediately upon selection.
- **Rationale:** We want the user to experience immediate feedback when applying a filter without needing an extra confirmation step.

### 3. Activity Filter Enum
Introduce a `ForumActivityFilter` enum to represent the different activity filters.
- **Rationale:** Using an enum provides type safety across the repository and provider layers compared to passing raw string values.

### 4. Data Layer Encapsulation
Pass the `ForumSort` and `ForumActivityFilter` enums natively through the provider layer, resolving them into raw primitive query parameters within the `ForumRepository` before passing them to the `DataSource`.
- **Rationale:** This maintains strict separation of concerns. The network data layer remains decoupled from domain-level enums. The caller (`ForumRepository`) is responsible for translating domain business logic into primitive values for the data source.

## Risks / Trade-offs

- **Risk:** The horizontal category chips might become crowded.
- **Mitigation:** The category chips are inside a horizontally scrollable list, allowing them to comfortably accommodate many categories.
- **Risk:** Existing instances of `globalForumFeedProvider` might break if the new activity parameter is not made optional.
- **Mitigation:** The new `activityFilter` parameter will be nullable (`ForumActivityFilter?`) and default to `null` to ensure backward compatibility with any other screens utilizing the feed.
27 changes: 27 additions & 0 deletions openspec/changes/add-forum-filters/proposal.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
## Why

Users currently lack an efficient way to filter discussion forum threads beyond basic category selection. A comprehensive filter menu is needed to allow filtering by user activity (e.g., "Posted by me", "Liked by me"), improving navigation and discoverability of relevant forum content.

## What Changes

- Add a filter icon to the `ForumPostsListScreen` header or search bar.
- Add a sorting segmented control containing tabs for "Recent", "Most Liked", and "Most Viewed" above the horizontal category chips.
- Retain the existing horizontal category chips.
- Introduce a Bottom Sheet (`ForumFilterBottomSheet`) that contains a new "Filter by Activity" section with options like "Posted by me", "Commented by me", "Liked by me", and "Bookmarked by me".
- Update the `globalForumFeedProvider` to accept and process an activity filter parameter and a sort parameter.
- **Backend Integration**: Implement a robust, layered data flow for the new filters by passing `ForumSort` and `ForumActivityFilter` enums seamlessly through the `GlobalForumFeed` provider, resolving them into primitive query parameters inside `ForumRepository`, and passing those raw values through the abstract `DataSource` contract to keep the network layer decoupled from the domain layer.

## Capabilities

### New Capabilities
- `forum-activity-filters`: Introduces filtering forum threads based on user activity (e.g., posted by me, commented by me, liked by me, bookmarked by me).
- `forum-sorting`: Introduces sorting forum threads by "Recent", "Most Liked", and "Most Viewed".

### Modified Capabilities
- `forum-post`: Updates the forum thread listing requirement to support advanced activity filtering through a Bottom Sheet and sorting through horizontal tabs.

## Impact

- **UI/UX**: Modifies the `ForumPostsListScreen` layout to include sorting tabs, and adds a Bottom Sheet for activity filter controls.
- **State Management**: Updates `GlobalForumFeed` provider to maintain activity filter state.
- **Data Layer**: Updates `ForumRepository.fetchThreads` and `DataSource.getForumThreads` API to support activity filtering parameters.
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
## ADDED Requirements

### Requirement: Activity Filtering
The system SHALL support filtering forum threads based on the user's activity.

#### Scenario: User selects an activity filter
- **WHEN** a user selects an activity filter (e.g., "Posted by me") from the filter bottom sheet
- **THEN** the system SHALL fetch and display only the threads matching that activity criteria
- **AND** the system SHALL update the feed provider to maintain the active activity filter state
13 changes: 13 additions & 0 deletions openspec/changes/add-forum-filters/specs/forum-post/spec.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
## ADDED Requirements

### Requirement: Thread Sorting and Filtering UI
The system SHALL provide horizontal tabs for sorting and a Bottom Sheet for advanced filtering.

#### Scenario: User navigates sorting tabs
- **WHEN** the user views the forum feed
- **THEN** the system SHALL display sorting tabs (Recent, Most Liked, Most Viewed) above the category chips
- **AND** tapping a tab SHALL update the feed order accordingly

#### Scenario: User opens filter bottom sheet
- **WHEN** user taps the filter icon on the thread list
- **THEN** the system SHALL open a Bottom Sheet containing Activity filters
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
## ADDED Requirements

### Requirement: Thread Sorting
The system SHALL support sorting the global forum feed threads.

#### Scenario: User selects a sort option
- **WHEN** a user selects a sort option (e.g., "Most Liked") from the segmented control
- **THEN** the system SHALL fetch and display the threads ordered by the selected sort criteria
- **AND** the system SHALL maintain the active sort order state within the feed provider
19 changes: 19 additions & 0 deletions openspec/changes/add-forum-filters/tasks.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
## 1. Data Layer & State Management

- [x] 1.1 Define `ForumActivityFilter` and `ForumSort` enums.
- [x] 1.2 Update `DataSource` interface and `MockDataSource`/`HttpDataSource` to accept primitive query parameters (strings/booleans) for filtering in `getForumThreads`.
- [x] 1.3 Update `ForumRepository.fetchThreads` to translate enums into raw parameters and pass them to `DataSource`.
- [x] 1.4 Update `GlobalForumFeed` Riverpod provider to maintain and accept `activityFilter` and `sortOrder` state.

## 2. UI Component: Activity Filter Bottom Sheet

- [x] 2.1 Create `ForumFilterBottomSheet` widget.
- [x] 2.2 Implement the "Filter by Activity" section with predefined options (e.g., Posted by me, Liked by me).
- [x] 2.3 Add "Clear" action to the bottom sheet to reset selections.

## 3. UI Integration: Main Screen

- [x] 3.1 Introduce a Sorting Segmented Control ("Recent", "Most Liked", "Most Viewed") above the horizontal `_CategoryChips` in `ForumPostsListScreen`.
- [x] 3.2 Connect the Sorting Segmented Control to update the `sortOrder` in `globalForumFeedProvider`.
- [x] 3.3 Add a filter icon to the `ForumPostsListScreen` header or adjacent to the search bar.
- [x] 3.4 Wire the filter icon to open `ForumFilterBottomSheet` via `AppBottomSheet`.
6 changes: 6 additions & 0 deletions packages/core/lib/data/models/forum_thread_dto.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
/// Forum thread status.
enum ForumThreadStatus { answered, unanswered, closed, archived, pending }

/// Forum activity filters for narrowing down threads.
enum ForumActivityFilter { posted, commented, liked, bookmarked }

/// Forum sorting options.
enum ForumSort { recent, mostLiked, mostViewed }

/// Forum category DTO — maps to `/api/v2.3/forum/categories/`.
class ForumCategoryDto {
final int id;
Expand Down
5 changes: 5 additions & 0 deletions packages/core/lib/data/sources/data_source.dart
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,11 @@ abstract class DataSource {
int page = 1,
int? categoryId,
String? searchQuery,
String? sortString,
bool? postedByMe,
bool? commentedByMe,
bool? likedByMe,
bool? bookmarkedByMe,
});

/// Fetch a single forum thread by slug.
Expand Down
10 changes: 10 additions & 0 deletions packages/core/lib/data/sources/http_data_source.dart
Original file line number Diff line number Diff line change
Expand Up @@ -272,6 +272,11 @@ class HttpDataSource implements DataSource {
int page = 1,
int? categoryId,
String? searchQuery,
String? sortString,
bool? postedByMe,
bool? commentedByMe,
bool? likedByMe,
bool? bookmarkedByMe,
}) async {
return performNetworkRequest(
_dio.get(
Expand All @@ -281,6 +286,11 @@ class HttpDataSource implements DataSource {
'category': ?categoryId,
if (searchQuery != null && searchQuery.isNotEmpty)
'search': searchQuery,
'sort': ?sortString,
'posted_by_me': ?postedByMe,
'commented_by_me': ?commentedByMe,
'liked_by_me': ?likedByMe,
'bookmarked_by_me': ?bookmarkedByMe,
},
),
fromJson: (json) => PaginatedResponseDto<ForumThreadDto>.fromJson(
Expand Down
48 changes: 47 additions & 1 deletion packages/core/lib/data/sources/mock_data_source.dart
Original file line number Diff line number Diff line change
Expand Up @@ -879,8 +879,54 @@ class MockDataSource implements DataSource {
int page = 1,
int? categoryId,
String? searchQuery,
String? sortString,
bool? postedByMe,
bool? commentedByMe,
bool? likedByMe,
bool? bookmarkedByMe,
}) async {
final results = mockForumThreads(page: page, categoryId: categoryId);
var results = mockForumThreads(page: page, categoryId: categoryId);

// Apply search filter
if (searchQuery != null && searchQuery.isNotEmpty) {
final query = searchQuery.toLowerCase();
results = results
.where(
(t) =>
t.title.toLowerCase().contains(query) ||
t.summary.toLowerCase().contains(query),
)
.toList();
}

// Apply activity filter (simulated)
if (postedByMe == true) {
results = results.where((t) => t.threadId % 2 == 0).toList();
} else if (commentedByMe == true) {
results = results.where((t) => t.replyCount > 2).toList();
} else if (likedByMe == true) {
results = results.where((t) => t.upvotes > 10).toList();
} else if (bookmarkedByMe == true) {
results = results.where((t) => t.threadId % 3 == 0).toList();
}

// Apply sort
if (sortString != null) {
results = List.from(results);
switch (sortString) {
case '-created':
results.sort((a, b) => b.threadId.compareTo(a.threadId));
break;
case '-upvotes':
results.sort((a, b) => b.upvotes.compareTo(a.upvotes));
break;
case '-views_count':
// Simulate most viewed by sorting by replies
results.sort((a, b) => b.replyCount.compareTo(a.replyCount));
break;
}
}

return PaginatedResponseDto<ForumThreadDto>(
results: results,
count: results.length * 5,
Expand Down
72 changes: 72 additions & 0 deletions packages/core/lib/generated/l10n/app_localizations.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2693,6 +2693,78 @@ abstract class AppLocalizations {
/// **'Discussion Forum'**
String get forumTitle;

/// No description provided for @forumFilterTitle.
///
/// In en, this message translates to:
/// **'Filters'**
String get forumFilterTitle;

/// No description provided for @forumFilterByActivity.
///
/// In en, this message translates to:
/// **'Filter by Activity'**
String get forumFilterByActivity;

/// No description provided for @forumFilterClearAll.
///
/// In en, this message translates to:
/// **'Clear All'**
String get forumFilterClearAll;

/// No description provided for @forumBackSemantic.
///
/// In en, this message translates to:
/// **'Back'**
String get forumBackSemantic;

/// No description provided for @forumFilterSemantic.
///
/// In en, this message translates to:
/// **'Filter'**
String get forumFilterSemantic;

/// No description provided for @forumFilterActivityPosted.
///
/// In en, this message translates to:
/// **'Posted by me'**
String get forumFilterActivityPosted;

/// No description provided for @forumFilterActivityCommented.
///
/// In en, this message translates to:
/// **'Commented by me'**
String get forumFilterActivityCommented;

/// No description provided for @forumFilterActivityLiked.
///
/// In en, this message translates to:
/// **'Liked by me'**
String get forumFilterActivityLiked;

/// No description provided for @forumFilterActivityBookmarked.
///
/// In en, this message translates to:
/// **'Bookmarked by me'**
String get forumFilterActivityBookmarked;

/// No description provided for @forumSortRecent.
///
/// In en, this message translates to:
/// **'Recent'**
String get forumSortRecent;

/// No description provided for @forumSortMostLiked.
///
/// In en, this message translates to:
/// **'Most Liked'**
String get forumSortMostLiked;

/// No description provided for @forumSortMostViewed.
///
/// In en, this message translates to:
/// **'Most Viewed'**
String get forumSortMostViewed;

/// No description provided for @forumSelectCourse.
///
/// In en, this message translates to:
Expand Down
36 changes: 36 additions & 0 deletions packages/core/lib/generated/l10n/app_localizations_ar.dart
Original file line number Diff line number Diff line change
Expand Up @@ -1434,6 +1434,42 @@ class AppLocalizationsAr extends AppLocalizations {
@override
String get forumTitle => 'منتدى المناقشة';

@override
String get forumFilterTitle => 'عوامل التصفية';

@override
String get forumFilterByActivity => 'تصفية حسب النشاط';

@override
String get forumFilterClearAll => 'مسح الكل';

@override
String get forumBackSemantic => 'عودة';

@override
String get forumFilterSemantic => 'تصفية';

@override
String get forumFilterActivityPosted => 'نشرتها أنا';

@override
String get forumFilterActivityCommented => 'علقت عليها أنا';

@override
String get forumFilterActivityLiked => 'أعجبتني';

@override
String get forumFilterActivityBookmarked => 'أشرت إليها كمرجعية';

@override
String get forumSortRecent => 'الأحدث';

@override
String get forumSortMostLiked => 'الأكثر إعجاباً';

@override
String get forumSortMostViewed => 'الأكثر مشاهدة';

@override
String get forumSelectCourse => 'اختر دورة لعرض المناقشات';

Expand Down
36 changes: 36 additions & 0 deletions packages/core/lib/generated/l10n/app_localizations_en.dart
Original file line number Diff line number Diff line change
Expand Up @@ -1437,6 +1437,42 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get forumTitle => 'Discussion Forum';

@override
String get forumFilterTitle => 'Filters';

@override
String get forumFilterByActivity => 'Filter by Activity';

@override
String get forumFilterClearAll => 'Clear All';

@override
String get forumBackSemantic => 'Back';

@override
String get forumFilterSemantic => 'Filter';

@override
String get forumFilterActivityPosted => 'Posted by me';

@override
String get forumFilterActivityCommented => 'Commented by me';

@override
String get forumFilterActivityLiked => 'Liked by me';

@override
String get forumFilterActivityBookmarked => 'Bookmarked by me';

@override
String get forumSortRecent => 'Recent';

@override
String get forumSortMostLiked => 'Most Liked';

@override
String get forumSortMostViewed => 'Most Viewed';

@override
String get forumSelectCourse => 'Select a course to view discussions';

Expand Down
Loading