mirror of
https://gitlab.com/mysocialportal/relatica
synced 2024-10-18 12:23:31 +00:00
Refactor post/comment views to use a more flattened structure.
- Will allow doing comment paging, limiting depth, etc.
This commit is contained in:
parent
45467295b8
commit
1f5232891f
7 changed files with 449 additions and 67 deletions
|
@ -1,46 +1,44 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_widget_from_html_core/flutter_widget_from_html_core.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import '../../models/entry_tree_item.dart';
|
||||
import '../../models/flattened_tree_item.dart';
|
||||
import '../../models/timeline_entry.dart';
|
||||
import '../../services/timeline_manager.dart';
|
||||
import '../../utils/url_opening_utils.dart';
|
||||
import '../media_attachment_viewer_control.dart';
|
||||
import '../padding.dart';
|
||||
import 'interactions_bar_control.dart';
|
||||
import 'status_header_control.dart';
|
||||
|
||||
class StatusControl extends StatefulWidget {
|
||||
final EntryTreeItem originalItem;
|
||||
class FlattenedTreeEntryControl extends StatefulWidget {
|
||||
final FlattenedTreeItem originalItem;
|
||||
final bool openRemote;
|
||||
final bool showStatusOpenButton;
|
||||
|
||||
const StatusControl(
|
||||
const FlattenedTreeEntryControl(
|
||||
{super.key,
|
||||
required this.originalItem,
|
||||
required this.openRemote,
|
||||
required this.showStatusOpenButton});
|
||||
|
||||
@override
|
||||
State<StatusControl> createState() => _StatusControlState();
|
||||
State<FlattenedTreeEntryControl> createState() => _StatusControlState();
|
||||
}
|
||||
|
||||
class _StatusControlState extends State<StatusControl> {
|
||||
static final _logger = Logger('$StatusControl');
|
||||
class _StatusControlState extends State<FlattenedTreeEntryControl> {
|
||||
static final _logger = Logger('$FlattenedTreeEntryControl');
|
||||
|
||||
var showContent = true;
|
||||
|
||||
var showComments = false;
|
||||
|
||||
EntryTreeItem get item => widget.originalItem;
|
||||
FlattenedTreeItem get item => widget.originalItem;
|
||||
|
||||
TimelineEntry get entry => item.entry;
|
||||
TimelineEntry get entry => item.timelineEntry;
|
||||
|
||||
bool get isPublic => item.entry.isPublic;
|
||||
bool get isPublic => entry.isPublic;
|
||||
|
||||
bool get isPost => item.entry.parentId.isEmpty;
|
||||
bool get isPost => entry.parentId.isEmpty;
|
||||
|
||||
bool get hasComments => entry.engagementSummary.repliesCount > 0;
|
||||
|
||||
|
@ -52,11 +50,14 @@ class _StatusControlState extends State<StatusControl> {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final manager = context.watch<TimelineManager>();
|
||||
_logger.finest('Building ${item.entry.toShortString()}');
|
||||
final padding = isPost ? 8.0 : 8.0;
|
||||
_logger.finest('Building ${entry.toShortString()}');
|
||||
const otherPadding = 8.0;
|
||||
final leftPadding = otherPadding + (widget.originalItem.level * 15.0);
|
||||
final color = widget.originalItem.level.isOdd
|
||||
? Theme.of(context).splashColor
|
||||
: Theme.of(context).cardColor;
|
||||
final body = Padding(
|
||||
padding: EdgeInsets.all(padding),
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
|
@ -95,28 +96,18 @@ class _StatusControlState extends State<StatusControl> {
|
|||
const VerticalPadding(
|
||||
height: 5,
|
||||
),
|
||||
if (isPost && hasComments)
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
setState(() {
|
||||
showComments = !showComments;
|
||||
});
|
||||
if (showComments) {
|
||||
await manager.refreshStatusChain(item.id);
|
||||
}
|
||||
},
|
||||
child:
|
||||
Text(showComments ? 'Hide Comments' : 'Load & Show Comments'),
|
||||
),
|
||||
if (item.totalChildren > 0 && showComments)
|
||||
buildChildComments(context),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
return isPost
|
||||
? body
|
||||
: Card(color: Theme.of(context).splashColor, child: body);
|
||||
return Padding(
|
||||
padding: EdgeInsets.only(
|
||||
left: leftPadding,
|
||||
right: otherPadding,
|
||||
top: otherPadding,
|
||||
bottom: otherPadding,
|
||||
),
|
||||
child: isPost ? body : Card(color: color, child: body),
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildBody(BuildContext context) {
|
||||
|
@ -145,31 +136,4 @@ class _StatusControlState extends State<StatusControl> {
|
|||
},
|
||||
itemCount: items.length));
|
||||
}
|
||||
|
||||
Widget buildChildComments(BuildContext context) {
|
||||
final comments = widget.originalItem.children;
|
||||
|
||||
if (comments.isEmpty) {
|
||||
return Text('No comments');
|
||||
}
|
||||
return Padding(
|
||||
padding: EdgeInsets.only(left: 5.0, top: 5.0),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
children: comments
|
||||
.map((c) => StatusControl(
|
||||
originalItem: c,
|
||||
openRemote: false,
|
||||
showStatusOpenButton: false,
|
||||
))
|
||||
.toList(),
|
||||
),
|
||||
),
|
||||
],
|
||||
));
|
||||
}
|
||||
}
|
79
lib/controls/timeline/post_control.dart
Normal file
79
lib/controls/timeline/post_control.dart
Normal file
|
@ -0,0 +1,79 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import '../../models/entry_tree_item.dart';
|
||||
import '../../models/timeline_entry.dart';
|
||||
import '../../services/timeline_manager.dart';
|
||||
import '../../utils/entry_tree_item_flattening.dart';
|
||||
import 'flattened_tree_entry_control.dart';
|
||||
|
||||
class PostControl extends StatefulWidget {
|
||||
final EntryTreeItem originalItem;
|
||||
final bool openRemote;
|
||||
final bool showStatusOpenButton;
|
||||
|
||||
const PostControl(
|
||||
{super.key,
|
||||
required this.originalItem,
|
||||
required this.openRemote,
|
||||
required this.showStatusOpenButton});
|
||||
|
||||
@override
|
||||
State<PostControl> createState() => _PostControlState();
|
||||
}
|
||||
|
||||
class _PostControlState extends State<PostControl> {
|
||||
static final _logger = Logger('$PostControl');
|
||||
|
||||
var showContent = true;
|
||||
|
||||
var showComments = false;
|
||||
|
||||
EntryTreeItem get item => widget.originalItem;
|
||||
|
||||
TimelineEntry get entry => item.entry;
|
||||
|
||||
bool get isPublic => item.entry.isPublic;
|
||||
|
||||
bool get hasComments => entry.engagementSummary.repliesCount > 0;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
showContent = entry.spoilerText.isEmpty;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final manager = context.watch<TimelineManager>();
|
||||
_logger.finest('Building ${item.entry.toShortString()}');
|
||||
final items = widget.originalItem.flatten(topLevelOnly: !showComments);
|
||||
final widgets = <Widget>[];
|
||||
for (var i = 0; i < items.length; i++) {
|
||||
final itemWidget = FlattenedTreeEntryControl(
|
||||
originalItem: items[i],
|
||||
openRemote: widget.openRemote,
|
||||
showStatusOpenButton: widget.showStatusOpenButton,
|
||||
);
|
||||
|
||||
widgets.add(itemWidget);
|
||||
if (i == 0 && hasComments) {
|
||||
widgets.add(
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
setState(() {
|
||||
showComments = !showComments;
|
||||
});
|
||||
if (showComments) {
|
||||
await manager.refreshStatusChain(entry.id);
|
||||
}
|
||||
},
|
||||
child:
|
||||
Text(showComments ? 'Hide Comments' : 'Load & Show Comments'),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
return Column(children: widgets);
|
||||
}
|
||||
}
|
|
@ -4,7 +4,7 @@ import 'package:provider/provider.dart';
|
|||
|
||||
import '../../models/TimelineIdentifiers.dart';
|
||||
import '../../services/timeline_manager.dart';
|
||||
import 'status_control.dart';
|
||||
import 'post_control.dart';
|
||||
|
||||
class TimelinePanel extends StatelessWidget {
|
||||
static final _logger = Logger('$TimelinePanel');
|
||||
|
@ -43,7 +43,7 @@ class TimelinePanel extends StatelessWidget {
|
|||
final item = items[itemIndex];
|
||||
_logger.finest(
|
||||
'Building item: $itemIndex: ${item.entry.toShortString()}');
|
||||
return StatusControl(
|
||||
return PostControl(
|
||||
originalItem: item,
|
||||
openRemote: false,
|
||||
showStatusOpenButton: true,
|
||||
|
|
15
lib/models/flattened_tree_item.dart
Normal file
15
lib/models/flattened_tree_item.dart
Normal file
|
@ -0,0 +1,15 @@
|
|||
import 'timeline_entry.dart';
|
||||
|
||||
class FlattenedTreeItem {
|
||||
final TimelineEntry timelineEntry;
|
||||
|
||||
final bool isMine;
|
||||
|
||||
final int level;
|
||||
|
||||
FlattenedTreeItem({
|
||||
required this.timelineEntry,
|
||||
required this.isMine,
|
||||
required this.level,
|
||||
});
|
||||
}
|
|
@ -1,7 +1,7 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import '../controls/timeline/status_control.dart';
|
||||
import '../controls/timeline/post_control.dart';
|
||||
import '../services/timeline_manager.dart';
|
||||
|
||||
class PostScreen extends StatelessWidget {
|
||||
|
@ -19,7 +19,7 @@ class PostScreen extends StatelessWidget {
|
|||
},
|
||||
child: SingleChildScrollView(
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
child: StatusControl(
|
||||
child: PostControl(
|
||||
originalItem: post,
|
||||
openRemote: true,
|
||||
showStatusOpenButton: true,
|
||||
|
|
53
lib/utils/entry_tree_item_flattening.dart
Normal file
53
lib/utils/entry_tree_item_flattening.dart
Normal file
|
@ -0,0 +1,53 @@
|
|||
import '../models/entry_tree_item.dart';
|
||||
import '../models/flattened_tree_item.dart';
|
||||
|
||||
extension FlatteningExtensions on EntryTreeItem {
|
||||
static const BaseLevel = 0;
|
||||
|
||||
List<FlattenedTreeItem> flatten(
|
||||
{int level = BaseLevel, bool topLevelOnly = false}) {
|
||||
final items = <FlattenedTreeItem>[];
|
||||
final myEntry = FlattenedTreeItem(
|
||||
timelineEntry: entry,
|
||||
isMine: isMine,
|
||||
level: level,
|
||||
);
|
||||
|
||||
items.add(myEntry);
|
||||
if (topLevelOnly) {
|
||||
return items;
|
||||
}
|
||||
|
||||
final sortedChildren = [...children];
|
||||
sortedChildren.sort((c1, c2) =>
|
||||
c1.entry.creationTimestamp.compareTo(c2.entry.creationTimestamp));
|
||||
for (final child in sortedChildren) {
|
||||
int childLevel = level + 1;
|
||||
if (child.entry.authorId == entry.authorId && level != BaseLevel) {
|
||||
childLevel = level;
|
||||
}
|
||||
|
||||
final childItems = child.flatten(level: childLevel);
|
||||
childItems.sort((c1, c2) {
|
||||
if (c2.level == c1.level) {
|
||||
return c1.timelineEntry.creationTimestamp
|
||||
.compareTo(c2.timelineEntry.creationTimestamp);
|
||||
}
|
||||
|
||||
if (c1.level == childLevel) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (c2.level == childLevel) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
});
|
||||
|
||||
items.addAll(childItems);
|
||||
}
|
||||
|
||||
return items;
|
||||
}
|
||||
}
|
271
test/flattened_tree_item_test.dart
Normal file
271
test/flattened_tree_item_test.dart
Normal file
|
@ -0,0 +1,271 @@
|
|||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:friendica_portal/globals.dart';
|
||||
import 'package:friendica_portal/models/entry_tree_item.dart';
|
||||
import 'package:friendica_portal/models/timeline_entry.dart';
|
||||
import 'package:friendica_portal/utils/entry_tree_item_flattening.dart';
|
||||
|
||||
void main() {
|
||||
group('Flattening Tests', () {
|
||||
test('Single entry no children', () {
|
||||
final entry = TimelineEntry.randomBuilt();
|
||||
final treeItem = EntryTreeItem(entry);
|
||||
final flattened = treeItem.flatten();
|
||||
expect(flattened.length, equals(1));
|
||||
expect(flattened.first.isMine, equals(treeItem.isMine));
|
||||
expect(flattened.first.timelineEntry, equals(treeItem.entry));
|
||||
});
|
||||
|
||||
test('Entry with two children', () {
|
||||
final post = TimelineEntry(id: '0');
|
||||
final children = {
|
||||
'1': EntryTreeItem(
|
||||
TimelineEntry(id: '1'),
|
||||
),
|
||||
'2': EntryTreeItem(
|
||||
TimelineEntry(id: '2'),
|
||||
),
|
||||
};
|
||||
final treeItem = EntryTreeItem(post, initialChildren: children);
|
||||
final flattened = treeItem.flatten();
|
||||
expect(flattened.length, equals(3));
|
||||
expect(
|
||||
flattened.map((e) => int.parse(e.timelineEntry.id)).toList(),
|
||||
equals([0, 1, 2]),
|
||||
);
|
||||
expect(
|
||||
flattened.map((e) => e.level).toList(),
|
||||
equals([0, 1, 1]),
|
||||
);
|
||||
});
|
||||
|
||||
test('Entry with nesting children different authors', () {
|
||||
final post = TimelineEntry(id: '0');
|
||||
final children = {
|
||||
'1': EntryTreeItem(TimelineEntry(id: '1', authorId: randomId()),
|
||||
initialChildren: {
|
||||
'2': EntryTreeItem(
|
||||
TimelineEntry(id: '2', authorId: randomId()),
|
||||
),
|
||||
}),
|
||||
};
|
||||
final treeItem = EntryTreeItem(post, initialChildren: children);
|
||||
final flattened = treeItem.flatten();
|
||||
expect(flattened.length, equals(3));
|
||||
expect(
|
||||
flattened.map((e) => int.parse(e.timelineEntry.id)).toList(),
|
||||
equals([0, 1, 2]),
|
||||
);
|
||||
expect(
|
||||
flattened.map((e) => e.level).toList(),
|
||||
equals([0, 1, 2]),
|
||||
);
|
||||
});
|
||||
|
||||
test('Entry with nesting children same authors', () {
|
||||
final post = TimelineEntry(id: '0');
|
||||
final children = {
|
||||
'1': EntryTreeItem(TimelineEntry(id: '1'), initialChildren: {
|
||||
'2': EntryTreeItem(
|
||||
TimelineEntry(id: '2'),
|
||||
),
|
||||
}),
|
||||
};
|
||||
final treeItem = EntryTreeItem(post, initialChildren: children);
|
||||
final flattened = treeItem.flatten();
|
||||
expect(flattened.length, equals(3));
|
||||
expect(
|
||||
flattened.map((e) => int.parse(e.timelineEntry.id)).toList(),
|
||||
equals([0, 1, 2]),
|
||||
);
|
||||
expect(
|
||||
flattened.map((e) => e.level).toList(),
|
||||
equals([0, 1, 1]),
|
||||
);
|
||||
});
|
||||
|
||||
test('Entry fully nested children', () {
|
||||
var stamp = 0;
|
||||
final post =
|
||||
TimelineEntry(id: '0', authorId: 'a0', creationTimestamp: stamp++);
|
||||
final children = {
|
||||
'1': EntryTreeItem(
|
||||
TimelineEntry(id: '1', creationTimestamp: stamp++),
|
||||
initialChildren: {
|
||||
'1.1': EntryTreeItem(
|
||||
TimelineEntry(
|
||||
id: '1.1',
|
||||
authorId: randomId(),
|
||||
creationTimestamp: stamp++,
|
||||
),
|
||||
),
|
||||
'1.2': EntryTreeItem(
|
||||
TimelineEntry(
|
||||
id: '1.2',
|
||||
authorId: randomId(),
|
||||
creationTimestamp: stamp++,
|
||||
),
|
||||
),
|
||||
'1.3': EntryTreeItem(
|
||||
TimelineEntry(
|
||||
id: '1.3',
|
||||
authorId: randomId(),
|
||||
creationTimestamp: stamp++,
|
||||
),
|
||||
),
|
||||
},
|
||||
),
|
||||
'2': EntryTreeItem(
|
||||
TimelineEntry(
|
||||
id: '2',
|
||||
authorId: randomId(),
|
||||
creationTimestamp: stamp++,
|
||||
),
|
||||
initialChildren: {
|
||||
'2.1': EntryTreeItem(
|
||||
TimelineEntry(
|
||||
id: '2.1',
|
||||
authorId: randomId(),
|
||||
creationTimestamp: stamp++,
|
||||
),
|
||||
),
|
||||
'2.2': EntryTreeItem(
|
||||
TimelineEntry(
|
||||
id: '2.2',
|
||||
authorId: randomId(),
|
||||
creationTimestamp: stamp++,
|
||||
),
|
||||
initialChildren: {
|
||||
'2.2.1': EntryTreeItem(
|
||||
TimelineEntry(
|
||||
id: '2.2.1',
|
||||
authorId: 'a1',
|
||||
creationTimestamp: stamp++,
|
||||
),
|
||||
initialChildren: {
|
||||
'2.2.1.1': EntryTreeItem(
|
||||
TimelineEntry(
|
||||
id: '2.2.1.1',
|
||||
creationTimestamp: stamp++,
|
||||
),
|
||||
),
|
||||
'2.2.1.2': EntryTreeItem(
|
||||
TimelineEntry(
|
||||
id: '2.2.1.2',
|
||||
authorId: 'a1',
|
||||
creationTimestamp: stamp++,
|
||||
),
|
||||
),
|
||||
},
|
||||
),
|
||||
'2.2.2': EntryTreeItem(
|
||||
TimelineEntry(
|
||||
id: '2.2.2',
|
||||
authorId: 'a2',
|
||||
creationTimestamp: stamp++,
|
||||
),
|
||||
initialChildren: {
|
||||
'2.2.2.1': EntryTreeItem(
|
||||
TimelineEntry(
|
||||
id: '2.2.2.1',
|
||||
creationTimestamp: (stamp++) + 100,
|
||||
),
|
||||
),
|
||||
'2.2.2.2': EntryTreeItem(
|
||||
TimelineEntry(
|
||||
id: '2.2.2.2',
|
||||
authorId: 'a2',
|
||||
creationTimestamp: stamp++,
|
||||
),
|
||||
),
|
||||
'2.2.2.3': EntryTreeItem(
|
||||
TimelineEntry(
|
||||
id: '2.2.2.3',
|
||||
authorId: 'a2',
|
||||
creationTimestamp: stamp++,
|
||||
),
|
||||
),
|
||||
'2.2.2.4': EntryTreeItem(
|
||||
TimelineEntry(
|
||||
id: '2.2.2.4',
|
||||
authorId: 'a0',
|
||||
creationTimestamp: stamp++,
|
||||
),
|
||||
),
|
||||
},
|
||||
),
|
||||
},
|
||||
),
|
||||
'2.3': EntryTreeItem(
|
||||
TimelineEntry(
|
||||
id: '2.3',
|
||||
authorId: randomId(),
|
||||
creationTimestamp: stamp++,
|
||||
),
|
||||
),
|
||||
},
|
||||
),
|
||||
'3': EntryTreeItem(
|
||||
TimelineEntry(
|
||||
id: '3',
|
||||
authorId: 'a0',
|
||||
creationTimestamp: stamp++,
|
||||
),
|
||||
initialChildren: {
|
||||
'3.1': EntryTreeItem(TimelineEntry(
|
||||
id: '3.1',
|
||||
authorId: randomId(),
|
||||
creationTimestamp: stamp++,
|
||||
)),
|
||||
'3.2': EntryTreeItem(
|
||||
TimelineEntry(
|
||||
id: '3.2',
|
||||
authorId: 'a0',
|
||||
creationTimestamp: stamp++,
|
||||
),
|
||||
),
|
||||
'3.3': EntryTreeItem(
|
||||
TimelineEntry(
|
||||
id: '3.3',
|
||||
authorId: randomId(),
|
||||
creationTimestamp: stamp++,
|
||||
),
|
||||
),
|
||||
},
|
||||
),
|
||||
};
|
||||
final treeItem = EntryTreeItem(post, initialChildren: children);
|
||||
final flattened = treeItem.flatten();
|
||||
expect(flattened.length, equals(21));
|
||||
expect(
|
||||
flattened.map((e) => e.timelineEntry.id).toList(),
|
||||
equals([
|
||||
'0',
|
||||
'1',
|
||||
'1.1',
|
||||
'1.2',
|
||||
'1.3',
|
||||
'2',
|
||||
'2.1',
|
||||
'2.2',
|
||||
'2.2.1',
|
||||
'2.2.1.2',
|
||||
'2.2.1.1',
|
||||
'2.2.2',
|
||||
'2.2.2.2',
|
||||
'2.2.2.3',
|
||||
'2.2.2.4',
|
||||
'2.2.2.1',
|
||||
'2.3',
|
||||
'3',
|
||||
'3.2',
|
||||
'3.1',
|
||||
'3.3',
|
||||
]),
|
||||
);
|
||||
expect(
|
||||
flattened.map((e) => e.level).toList(),
|
||||
equals([0, 1, 2, 2, 2, 1, 2, 2, 3, 3, 4, 3, 3, 3, 4, 4, 2, 1, 1, 2, 2]),
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
Loading…
Reference in a new issue