19bd8c1810
CI / flutter-gates (push) Successful in 2m26s
- community_models:community/media 域 DTO 逐字段照冻结契约手写 JSON 映射 (Post/FeedCard/PostComment/AuthorSummary 降级形态/媒体两步上传凭据等), 未知枚举抛 FormatException 暴露契约漂移;CursorPage 上移 core 复用 - community_repository:13 路径 19 操作全覆盖;createPost/createComment 必带 Idempotency-Key(每次逻辑提交换新键、刷新重放同键);点赞/收藏/ 关注走 PUT/DELETE 语义幂等 - community_exceptions:v1.3.0 新增 9 码 + 40902 共码类型化异常映射 - toggle_sync:乐观翻转 + 快照回滚 + 单飞合并最终意图 + 代次守卫, 点赞/收藏共用一套参数化状态机,权威终态对账收敛 - community_controller:Feed 多页缓存 + 首屏四态 + 尾部加载三态 + 游标拼接 + 刷新代次丢弃旧尾页;详情副本与卡片互动状态同源;reset 清态 - app.dart 装配:community 服务分端口直连(:8084),共享 TokenRefresher, 登出同步 reset - 测试 286 → 347(模型映射 / 19 操作线路 / 错误映射 / 竞态序列全覆盖) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
631 lines
19 KiB
Dart
631 lines
19 KiB
Dart
/// community / media 域响应 / 请求模型(接口契约冻结稿 openapi.yaml v1.3.0,
|
||
/// 字段名与后端逐字一致;枚举取值严格校验,未知值抛 [FormatException]
|
||
/// 以便契约漂移在测试期暴露而非静默吞掉)。
|
||
library;
|
||
|
||
export 'package:patbond_flutter/core/models/cursor_page.dart';
|
||
|
||
T _enumFromJson<T extends Enum>(List<T> values, String raw, String field) {
|
||
for (final value in values) {
|
||
if (value.name == raw) return value;
|
||
}
|
||
throw FormatException('未知的 $field 取值:$raw');
|
||
}
|
||
|
||
DateTime? _dateTimeOrNull(Object? value) =>
|
||
value == null ? null : DateTime.parse(value as String);
|
||
|
||
/// 帖子分类。ai_creation 为 M4 预留值,仅读侧出现(M3 提交即 400/40000)。
|
||
enum PostCategory {
|
||
general('general'),
|
||
help('help'),
|
||
aiCreation('ai_creation');
|
||
|
||
const PostCategory(this.wire);
|
||
|
||
/// 契约线上取值(aiCreation 的枚举名与线上 snake_case 不同,序列化走本值)。
|
||
final String wire;
|
||
|
||
static PostCategory fromJson(String value) {
|
||
for (final category in values) {
|
||
if (category.wire == value) return category;
|
||
}
|
||
throw FormatException('未知的 category 取值:$value');
|
||
}
|
||
}
|
||
|
||
/// 帖子状态。hidden/archived(运营态)永不出现在响应(对作者与他人一律
|
||
/// 404/40403),枚举保持两值。
|
||
enum PostStatus {
|
||
draft,
|
||
published;
|
||
|
||
static PostStatus fromJson(String value) =>
|
||
_enumFromJson(values, value, 'status');
|
||
}
|
||
|
||
/// 帖子可见性。M3 恒 public(followers/private 语义后置,字段保留)。
|
||
enum PostVisibility {
|
||
public;
|
||
|
||
static PostVisibility fromJson(String value) =>
|
||
_enumFromJson(values, value, 'visibility');
|
||
}
|
||
|
||
/// media asset 类型。M3 仅 image(视频后置,video/document 为向后新增预留)。
|
||
enum MediaKind {
|
||
image;
|
||
|
||
static MediaKind fromJson(String value) =>
|
||
_enumFromJson(values, value, 'kind');
|
||
}
|
||
|
||
/// 上传用途白名单(M3 定型仅 post_image,决定 objectKey 前缀)。
|
||
enum MediaPurpose {
|
||
postImage('post_image');
|
||
|
||
const MediaPurpose(this.wire);
|
||
|
||
final String wire;
|
||
}
|
||
|
||
/// media asset 状态。deleted 态对外恒 404/40405,不出现在响应。
|
||
enum MediaAssetStatus {
|
||
uploading,
|
||
ready,
|
||
failed;
|
||
|
||
static MediaAssetStatus fromJson(String value) =>
|
||
_enumFromJson(values, value, 'status');
|
||
}
|
||
|
||
/// 作者公开摘要(D3-9 方案 B)。正常路径 nickname 恒非空(空昵称由服务端
|
||
/// 回退为 username,客户端不做回退拼装);nickname 与 avatarUrl 同为 null
|
||
/// 即「降级/墓碑」形态(作者资料暂不可得或已注销),客户端只需一种占位逻辑。
|
||
class AuthorSummary {
|
||
const AuthorSummary({
|
||
required this.userId,
|
||
required this.nickname,
|
||
required this.avatarUrl,
|
||
});
|
||
|
||
factory AuthorSummary.fromJson(Map<String, dynamic> json) {
|
||
return AuthorSummary(
|
||
userId: json['userId'] as String,
|
||
nickname: json['nickname'] as String?,
|
||
// 时效性预签名 GET URL,每次响应现签,不得持久化、过期即重取。
|
||
avatarUrl: json['avatarUrl'] as String?,
|
||
);
|
||
}
|
||
|
||
final String userId;
|
||
final String? nickname;
|
||
final String? avatarUrl;
|
||
|
||
/// 降级/墓碑形态(id-only):页面渲染统一占位。
|
||
bool get isDegraded => nickname == null && avatarUrl == null;
|
||
}
|
||
|
||
/// 帖子挂接的一张图(响应形态)。url 为时效性预签名 GET(TTL 默认 1 小时),
|
||
/// 每次响应现签,客户端不得持久化、过期即重取。
|
||
class PostMediaItem {
|
||
const PostMediaItem({
|
||
required this.assetId,
|
||
required this.position,
|
||
required this.isCover,
|
||
required this.url,
|
||
required this.widthPx,
|
||
required this.heightPx,
|
||
required this.caption,
|
||
});
|
||
|
||
factory PostMediaItem.fromJson(Map<String, dynamic> json) {
|
||
return PostMediaItem(
|
||
assetId: json['assetId'] as String,
|
||
position: json['position'] as int,
|
||
isCover: json['isCover'] as bool,
|
||
url: json['url'] as String,
|
||
widthPx: json['widthPx'] as int?,
|
||
heightPx: json['heightPx'] as int?,
|
||
caption: json['caption'] as String?,
|
||
);
|
||
}
|
||
|
||
final String assetId;
|
||
final int position;
|
||
final bool isCover;
|
||
final String url;
|
||
final int? widthPx;
|
||
final int? heightPx;
|
||
final String? caption;
|
||
}
|
||
|
||
/// 帖子挂接的一张图(请求形态)。position 全给或全不给(全给须恰为 0..n-1
|
||
/// 连续不重复,混合 400/40000);isCover 至多一个 true;caption trim 后 ≤300。
|
||
class PostMediaAttachRequest {
|
||
const PostMediaAttachRequest({
|
||
required this.assetId,
|
||
this.position,
|
||
this.isCover,
|
||
this.caption,
|
||
});
|
||
|
||
final String assetId;
|
||
final int? position;
|
||
final bool? isCover;
|
||
final String? caption;
|
||
|
||
Map<String, Object?> toJson() => {
|
||
'assetId': assetId,
|
||
if (position != null) 'position': position,
|
||
if (isCover != null) 'isCover': isCover,
|
||
if (caption != null) 'caption': caption,
|
||
};
|
||
}
|
||
|
||
/// 创建帖子请求。content 必填(纯文字帖合法,media 空数组或缺席);
|
||
/// status=published 即创建即发布(服务端写 publishedAt)。
|
||
class CreatePostRequest {
|
||
const CreatePostRequest({
|
||
required this.content,
|
||
this.title,
|
||
this.category,
|
||
this.status,
|
||
this.petId,
|
||
this.media,
|
||
});
|
||
|
||
final String content;
|
||
final String? title;
|
||
final PostCategory? category;
|
||
final PostStatus? status;
|
||
final String? petId;
|
||
final List<PostMediaAttachRequest>? media;
|
||
|
||
Map<String, Object?> toJson() => {
|
||
'content': content,
|
||
if (title != null) 'title': title,
|
||
if (category != null) 'category': category!.wire,
|
||
if (status != null) 'status': status!.name,
|
||
if (petId != null) 'petId': petId,
|
||
if (media != null) 'media': media!.map((item) => item.toJson()).toList(),
|
||
};
|
||
}
|
||
|
||
/// 编辑帖子 / 发布草稿请求(部分更新:缺席字段不变,不支持清空回 null;
|
||
/// version 乐观锁必带)。
|
||
///
|
||
/// - [publish]:`status: published` 状态迁移(draft→published 唯一开放迁移;
|
||
/// 对已发布帖重复提交为幂等 no-op,弱网重发不报错)。
|
||
/// - [media] 三态:null = 缺席不动;`[]` = 清空为纯文字帖;非空 = 整组替换。
|
||
class UpdatePostRequest {
|
||
const UpdatePostRequest({
|
||
required this.version,
|
||
this.title,
|
||
this.content,
|
||
this.category,
|
||
this.petId,
|
||
this.publish = false,
|
||
this.media,
|
||
});
|
||
|
||
final int version;
|
||
final String? title;
|
||
final String? content;
|
||
final PostCategory? category;
|
||
final String? petId;
|
||
final bool publish;
|
||
final List<PostMediaAttachRequest>? media;
|
||
|
||
Map<String, Object?> toJson() => {
|
||
'version': version,
|
||
if (title != null) 'title': title,
|
||
if (content != null) 'content': content,
|
||
if (category != null) 'category': category!.wire,
|
||
if (petId != null) 'petId': petId,
|
||
if (publish) 'status': PostStatus.published.name,
|
||
if (media != null) 'media': media!.map((item) => item.toJson()).toList(),
|
||
};
|
||
}
|
||
|
||
/// 帖子完整形态(详情 / 我的帖子列表 / 写响应共用)。
|
||
/// 「内容是否编辑过」以 version 为准(互动计数维护亦会推动 updatedAt)。
|
||
class Post {
|
||
const Post({
|
||
required this.id,
|
||
required this.author,
|
||
required this.petId,
|
||
required this.category,
|
||
required this.title,
|
||
required this.content,
|
||
required this.status,
|
||
required this.visibility,
|
||
required this.media,
|
||
required this.likeCount,
|
||
required this.commentCount,
|
||
required this.bookmarkCount,
|
||
required this.likedByMe,
|
||
required this.bookmarkedByMe,
|
||
required this.createdAt,
|
||
required this.updatedAt,
|
||
required this.publishedAt,
|
||
required this.version,
|
||
});
|
||
|
||
factory Post.fromJson(Map<String, dynamic> json) {
|
||
return Post(
|
||
id: json['id'] as String,
|
||
author: AuthorSummary.fromJson(json['author'] as Map<String, dynamic>),
|
||
petId: json['petId'] as String?,
|
||
category: PostCategory.fromJson(json['category'] as String),
|
||
title: json['title'] as String?,
|
||
content: json['content'] as String,
|
||
status: PostStatus.fromJson(json['status'] as String),
|
||
visibility: PostVisibility.fromJson(json['visibility'] as String),
|
||
media: (json['media'] as List)
|
||
.map((item) => PostMediaItem.fromJson(item as Map<String, dynamic>))
|
||
.toList(),
|
||
likeCount: json['likeCount'] as int,
|
||
commentCount: json['commentCount'] as int,
|
||
bookmarkCount: json['bookmarkCount'] as int,
|
||
likedByMe: json['likedByMe'] as bool,
|
||
bookmarkedByMe: json['bookmarkedByMe'] as bool,
|
||
createdAt: DateTime.parse(json['createdAt'] as String),
|
||
updatedAt: DateTime.parse(json['updatedAt'] as String),
|
||
// 仅 published 非空(发布时恰写一次)。
|
||
publishedAt: _dateTimeOrNull(json['publishedAt']),
|
||
version: json['version'] as int,
|
||
);
|
||
}
|
||
|
||
final String id;
|
||
final AuthorSummary author;
|
||
final String? petId;
|
||
final PostCategory category;
|
||
final String? title;
|
||
final String content;
|
||
final PostStatus status;
|
||
final PostVisibility visibility;
|
||
final List<PostMediaItem> media;
|
||
final int likeCount;
|
||
final int commentCount;
|
||
final int bookmarkCount;
|
||
final bool likedByMe;
|
||
final bool bookmarkedByMe;
|
||
final DateTime createdAt;
|
||
final DateTime updatedAt;
|
||
final DateTime? publishedAt;
|
||
final int version;
|
||
|
||
/// 互动字段副本更新(乐观翻转 / 权威终态对账用,其余字段不变)。
|
||
Post copyWithInteraction({
|
||
int? likeCount,
|
||
int? commentCount,
|
||
int? bookmarkCount,
|
||
bool? likedByMe,
|
||
bool? bookmarkedByMe,
|
||
}) {
|
||
return Post(
|
||
id: id,
|
||
author: author,
|
||
petId: petId,
|
||
category: category,
|
||
title: title,
|
||
content: content,
|
||
status: status,
|
||
visibility: visibility,
|
||
media: media,
|
||
likeCount: likeCount ?? this.likeCount,
|
||
commentCount: commentCount ?? this.commentCount,
|
||
bookmarkCount: bookmarkCount ?? this.bookmarkCount,
|
||
likedByMe: likedByMe ?? this.likedByMe,
|
||
bookmarkedByMe: bookmarkedByMe ?? this.bookmarkedByMe,
|
||
createdAt: createdAt,
|
||
updatedAt: updatedAt,
|
||
publishedAt: publishedAt,
|
||
version: version,
|
||
);
|
||
}
|
||
}
|
||
|
||
/// Feed / 收藏列表卡片形态(较 Post 裁剪:只带 coverImage + mediaCount,
|
||
/// 全文恒走帖子详情端点)。publishedAt 恒非空(谓词只放行 published)。
|
||
class FeedCard {
|
||
const FeedCard({
|
||
required this.id,
|
||
required this.author,
|
||
required this.category,
|
||
required this.title,
|
||
required this.contentPreview,
|
||
required this.coverImage,
|
||
required this.mediaCount,
|
||
required this.likeCount,
|
||
required this.commentCount,
|
||
required this.bookmarkCount,
|
||
required this.likedByMe,
|
||
required this.bookmarkedByMe,
|
||
required this.publishedAt,
|
||
});
|
||
|
||
factory FeedCard.fromJson(Map<String, dynamic> json) {
|
||
final cover = json['coverImage'];
|
||
return FeedCard(
|
||
id: json['id'] as String,
|
||
author: AuthorSummary.fromJson(json['author'] as Map<String, dynamic>),
|
||
category: PostCategory.fromJson(json['category'] as String),
|
||
title: json['title'] as String?,
|
||
contentPreview: json['contentPreview'] as String,
|
||
// 封面 = 库中唯一 is_cover 行;纯文字帖为 null。
|
||
coverImage: cover == null
|
||
? null
|
||
: PostMediaItem.fromJson(cover as Map<String, dynamic>),
|
||
mediaCount: json['mediaCount'] as int,
|
||
likeCount: json['likeCount'] as int,
|
||
commentCount: json['commentCount'] as int,
|
||
bookmarkCount: json['bookmarkCount'] as int,
|
||
likedByMe: json['likedByMe'] as bool,
|
||
bookmarkedByMe: json['bookmarkedByMe'] as bool,
|
||
publishedAt: DateTime.parse(json['publishedAt'] as String),
|
||
);
|
||
}
|
||
|
||
final String id;
|
||
final AuthorSummary author;
|
||
final PostCategory category;
|
||
final String? title;
|
||
final String contentPreview;
|
||
final PostMediaItem? coverImage;
|
||
final int mediaCount;
|
||
final int likeCount;
|
||
final int commentCount;
|
||
final int bookmarkCount;
|
||
final bool likedByMe;
|
||
final bool bookmarkedByMe;
|
||
final DateTime publishedAt;
|
||
|
||
/// 互动字段副本更新(乐观翻转 / 权威终态对账用,其余字段不变)。
|
||
FeedCard copyWithInteraction({
|
||
int? likeCount,
|
||
int? commentCount,
|
||
int? bookmarkCount,
|
||
bool? likedByMe,
|
||
bool? bookmarkedByMe,
|
||
}) {
|
||
return FeedCard(
|
||
id: id,
|
||
author: author,
|
||
category: category,
|
||
title: title,
|
||
contentPreview: contentPreview,
|
||
coverImage: coverImage,
|
||
mediaCount: mediaCount,
|
||
likeCount: likeCount ?? this.likeCount,
|
||
commentCount: commentCount ?? this.commentCount,
|
||
bookmarkCount: bookmarkCount ?? this.bookmarkCount,
|
||
likedByMe: likedByMe ?? this.likedByMe,
|
||
bookmarkedByMe: bookmarkedByMe ?? this.bookmarkedByMe,
|
||
publishedAt: publishedAt,
|
||
);
|
||
}
|
||
}
|
||
|
||
/// 创建评论请求。content trim 后 1~2000;replyToUserId 可选 @ 回复目标
|
||
/// (单层平铺,无楼中楼)。
|
||
class CreateCommentRequest {
|
||
const CreateCommentRequest({required this.content, this.replyToUserId});
|
||
|
||
final String content;
|
||
final String? replyToUserId;
|
||
|
||
Map<String, Object?> toJson() => {
|
||
'content': content,
|
||
if (replyToUserId != null) 'replyToUserId': replyToUserId,
|
||
};
|
||
}
|
||
|
||
/// 评论(M3 无评论编辑,不带 updatedAt)。
|
||
class PostComment {
|
||
const PostComment({
|
||
required this.id,
|
||
required this.postId,
|
||
required this.author,
|
||
required this.replyToUser,
|
||
required this.content,
|
||
required this.createdAt,
|
||
});
|
||
|
||
factory PostComment.fromJson(Map<String, dynamic> json) {
|
||
final replyTo = json['replyToUser'];
|
||
return PostComment(
|
||
id: json['id'] as String,
|
||
postId: json['postId'] as String,
|
||
author: AuthorSummary.fromJson(json['author'] as Map<String, dynamic>),
|
||
// @ 回复目标公开摘要(含降级 id-only 形态);非回复为 null。
|
||
replyToUser: replyTo == null
|
||
? null
|
||
: AuthorSummary.fromJson(replyTo as Map<String, dynamic>),
|
||
content: json['content'] as String,
|
||
createdAt: DateTime.parse(json['createdAt'] as String),
|
||
);
|
||
}
|
||
|
||
final String id;
|
||
final String postId;
|
||
final AuthorSummary author;
|
||
final AuthorSummary? replyToUser;
|
||
final String content;
|
||
final DateTime createdAt;
|
||
}
|
||
|
||
/// 创建上传请求(两步上传第一步)。mimeType 白名单
|
||
/// image/jpeg|png|webp,byteSize ≤ 10485760(10 MiB,服务端配置项);
|
||
/// sha256 可选(64 位小写 hex,M3 照收照存不核验)。
|
||
class CreateMediaUploadRequest {
|
||
const CreateMediaUploadRequest({
|
||
required this.kind,
|
||
required this.purpose,
|
||
required this.mimeType,
|
||
required this.byteSize,
|
||
this.sha256,
|
||
});
|
||
|
||
final MediaKind kind;
|
||
final MediaPurpose purpose;
|
||
final String mimeType;
|
||
final int byteSize;
|
||
final String? sha256;
|
||
|
||
Map<String, Object?> toJson() => {
|
||
'kind': kind.name,
|
||
'purpose': purpose.wire,
|
||
'mimeType': mimeType,
|
||
'byteSize': byteSize,
|
||
if (sha256 != null) 'sha256': sha256,
|
||
};
|
||
}
|
||
|
||
/// 预签名直传凭据。凭据(uploadUrl 含签名)TTL 默认 10 分钟,过期后
|
||
/// 重新创建上传;直传必须原样携带 requiredHeaders(Content-Type 已签进
|
||
/// 签名,改动即被存储侧拒绝)。凭据会过期,不得持久化。
|
||
class MediaUploadCredentials {
|
||
const MediaUploadCredentials({
|
||
required this.assetId,
|
||
required this.uploadUrl,
|
||
required this.method,
|
||
required this.requiredHeaders,
|
||
required this.expiresAt,
|
||
});
|
||
|
||
factory MediaUploadCredentials.fromJson(Map<String, dynamic> json) {
|
||
return MediaUploadCredentials(
|
||
assetId: json['assetId'] as String,
|
||
uploadUrl: json['uploadUrl'] as String,
|
||
method: json['method'] as String,
|
||
requiredHeaders: (json['requiredHeaders'] as Map<String, dynamic>).map(
|
||
(key, value) => MapEntry(key, value as String),
|
||
),
|
||
expiresAt: DateTime.parse(json['expiresAt'] as String),
|
||
);
|
||
}
|
||
|
||
final String assetId;
|
||
final String uploadUrl;
|
||
|
||
/// 契约定型恒为 PUT(enum 单值;直传时按本值发起请求)。
|
||
final String method;
|
||
final Map<String, String> requiredHeaders;
|
||
final DateTime expiresAt;
|
||
}
|
||
|
||
/// media asset(confirm 后的可引用形态)。url 仅 ready 态非空——时效性
|
||
/// 预签名 GET(TTL 默认 1 小时),每次响应现签,不得持久化、过期即重取。
|
||
class MediaAsset {
|
||
const MediaAsset({
|
||
required this.id,
|
||
required this.kind,
|
||
required this.purpose,
|
||
required this.mimeType,
|
||
required this.byteSize,
|
||
required this.widthPx,
|
||
required this.heightPx,
|
||
required this.status,
|
||
required this.url,
|
||
required this.readyAt,
|
||
required this.createdAt,
|
||
});
|
||
|
||
factory MediaAsset.fromJson(Map<String, dynamic> json) {
|
||
return MediaAsset(
|
||
id: json['id'] as String,
|
||
kind: MediaKind.fromJson(json['kind'] as String),
|
||
purpose: json['purpose'] as String,
|
||
mimeType: json['mimeType'] as String,
|
||
byteSize: json['byteSize'] as int?,
|
||
widthPx: json['widthPx'] as int?,
|
||
heightPx: json['heightPx'] as int?,
|
||
status: MediaAssetStatus.fromJson(json['status'] as String),
|
||
url: json['url'] as String?,
|
||
readyAt: _dateTimeOrNull(json['readyAt']),
|
||
createdAt: DateTime.parse(json['createdAt'] as String),
|
||
);
|
||
}
|
||
|
||
final String id;
|
||
final MediaKind kind;
|
||
final String purpose;
|
||
final String mimeType;
|
||
final int? byteSize;
|
||
final int? widthPx;
|
||
final int? heightPx;
|
||
final MediaAssetStatus status;
|
||
final String? url;
|
||
final DateTime? readyAt;
|
||
final DateTime createdAt;
|
||
}
|
||
|
||
/// 点赞权威终态(乐观更新以此对账回滚,回滚基准取响应值)。
|
||
class LikeState {
|
||
const LikeState({required this.liked, required this.likeCount});
|
||
|
||
factory LikeState.fromJson(Map<String, dynamic> json) {
|
||
return LikeState(
|
||
liked: json['liked'] as bool,
|
||
likeCount: json['likeCount'] as int,
|
||
);
|
||
}
|
||
|
||
final bool liked;
|
||
final int likeCount;
|
||
}
|
||
|
||
/// 收藏权威终态(与点赞同构)。
|
||
class BookmarkState {
|
||
const BookmarkState({required this.bookmarked, required this.bookmarkCount});
|
||
|
||
factory BookmarkState.fromJson(Map<String, dynamic> json) {
|
||
return BookmarkState(
|
||
bookmarked: json['bookmarked'] as bool,
|
||
bookmarkCount: json['bookmarkCount'] as int,
|
||
);
|
||
}
|
||
|
||
final bool bookmarked;
|
||
final int bookmarkCount;
|
||
}
|
||
|
||
/// 关注权威终态;followerCount 为目标用户的粉丝数(实时 COUNT)。
|
||
class FollowState {
|
||
const FollowState({required this.following, required this.followerCount});
|
||
|
||
factory FollowState.fromJson(Map<String, dynamic> json) {
|
||
return FollowState(
|
||
following: json['following'] as bool,
|
||
followerCount: json['followerCount'] as int,
|
||
);
|
||
}
|
||
|
||
final bool following;
|
||
final int followerCount;
|
||
}
|
||
|
||
/// 关注计数(关注数 / 粉丝数 / 我是否已关注;查自己 followedByMe 恒 false)。
|
||
class FollowStats {
|
||
const FollowStats({
|
||
required this.followerCount,
|
||
required this.followingCount,
|
||
required this.followedByMe,
|
||
});
|
||
|
||
factory FollowStats.fromJson(Map<String, dynamic> json) {
|
||
return FollowStats(
|
||
followerCount: json['followerCount'] as int,
|
||
followingCount: json['followingCount'] as int,
|
||
followedByMe: json['followedByMe'] as bool,
|
||
);
|
||
}
|
||
|
||
final int followerCount;
|
||
final int followingCount;
|
||
final bool followedByMe;
|
||
}
|