✨ Basic large screen support
This commit is contained in:
		@@ -28,32 +28,32 @@ class Account {
 | 
				
			|||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  factory Account.fromJson(Map<String, dynamic> json) => Account(
 | 
					  factory Account.fromJson(Map<String, dynamic> json) => Account(
 | 
				
			||||||
        id: json["id"],
 | 
					        id: json['id'],
 | 
				
			||||||
        createdAt: DateTime.parse(json["created_at"]),
 | 
					        createdAt: DateTime.parse(json['created_at']),
 | 
				
			||||||
        updatedAt: DateTime.parse(json["updated_at"]),
 | 
					        updatedAt: DateTime.parse(json['updated_at']),
 | 
				
			||||||
        deletedAt: json["deleted_at"],
 | 
					        deletedAt: json['deleted_at'],
 | 
				
			||||||
        name: json["name"],
 | 
					        name: json['name'],
 | 
				
			||||||
        nick: json["nick"],
 | 
					        nick: json['nick'],
 | 
				
			||||||
        avatar: json["avatar"],
 | 
					        avatar: json['avatar'],
 | 
				
			||||||
        banner: json["banner"],
 | 
					        banner: json['banner'],
 | 
				
			||||||
        description: json["description"],
 | 
					        description: json['description'],
 | 
				
			||||||
        emailAddress: json["email_address"],
 | 
					        emailAddress: json['email_address'],
 | 
				
			||||||
        powerLevel: json["power_level"],
 | 
					        powerLevel: json['power_level'],
 | 
				
			||||||
        externalId: json["external_id"],
 | 
					        externalId: json['external_id'],
 | 
				
			||||||
      );
 | 
					      );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  Map<String, dynamic> toJson() => {
 | 
					  Map<String, dynamic> toJson() => {
 | 
				
			||||||
        "id": id,
 | 
					        'id': id,
 | 
				
			||||||
        "created_at": createdAt.toIso8601String(),
 | 
					        'created_at': createdAt.toIso8601String(),
 | 
				
			||||||
        "updated_at": updatedAt.toIso8601String(),
 | 
					        'updated_at': updatedAt.toIso8601String(),
 | 
				
			||||||
        "deleted_at": deletedAt,
 | 
					        'deleted_at': deletedAt,
 | 
				
			||||||
        "name": name,
 | 
					        'name': name,
 | 
				
			||||||
        "nick": nick,
 | 
					        'nick': nick,
 | 
				
			||||||
        "avatar": avatar,
 | 
					        'avatar': avatar,
 | 
				
			||||||
        "banner": banner,
 | 
					        'banner': banner,
 | 
				
			||||||
        "description": description,
 | 
					        'description': description,
 | 
				
			||||||
        "email_address": emailAddress,
 | 
					        'email_address': emailAddress,
 | 
				
			||||||
        "power_level": powerLevel,
 | 
					        'power_level': powerLevel,
 | 
				
			||||||
        "external_id": externalId,
 | 
					        'external_id': externalId,
 | 
				
			||||||
      };
 | 
					      };
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -28,32 +28,32 @@ class Author {
 | 
				
			|||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  factory Author.fromJson(Map<String, dynamic> json) => Author(
 | 
					  factory Author.fromJson(Map<String, dynamic> json) => Author(
 | 
				
			||||||
        id: json["id"],
 | 
					        id: json['id'],
 | 
				
			||||||
        createdAt: DateTime.parse(json["created_at"]),
 | 
					        createdAt: DateTime.parse(json['created_at']),
 | 
				
			||||||
        updatedAt: DateTime.parse(json["updated_at"]),
 | 
					        updatedAt: DateTime.parse(json['updated_at']),
 | 
				
			||||||
        deletedAt: json["deleted_at"],
 | 
					        deletedAt: json['deleted_at'],
 | 
				
			||||||
        name: json["name"],
 | 
					        name: json['name'],
 | 
				
			||||||
        nick: json["nick"],
 | 
					        nick: json['nick'],
 | 
				
			||||||
        avatar: json["avatar"],
 | 
					        avatar: json['avatar'],
 | 
				
			||||||
        banner: json["banner"],
 | 
					        banner: json['banner'],
 | 
				
			||||||
        description: json["description"],
 | 
					        description: json['description'],
 | 
				
			||||||
        emailAddress: json["email_address"],
 | 
					        emailAddress: json['email_address'],
 | 
				
			||||||
        powerLevel: json["power_level"],
 | 
					        powerLevel: json['power_level'],
 | 
				
			||||||
        externalId: json["external_id"],
 | 
					        externalId: json['external_id'],
 | 
				
			||||||
      );
 | 
					      );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  Map<String, dynamic> toJson() => {
 | 
					  Map<String, dynamic> toJson() => {
 | 
				
			||||||
        "id": id,
 | 
					        'id': id,
 | 
				
			||||||
        "created_at": createdAt.toIso8601String(),
 | 
					        'created_at': createdAt.toIso8601String(),
 | 
				
			||||||
        "updated_at": updatedAt.toIso8601String(),
 | 
					        'updated_at': updatedAt.toIso8601String(),
 | 
				
			||||||
        "deleted_at": deletedAt,
 | 
					        'deleted_at': deletedAt,
 | 
				
			||||||
        "name": name,
 | 
					        'name': name,
 | 
				
			||||||
        "nick": nick,
 | 
					        'nick': nick,
 | 
				
			||||||
        "avatar": avatar,
 | 
					        'avatar': avatar,
 | 
				
			||||||
        "banner": banner,
 | 
					        'banner': banner,
 | 
				
			||||||
        "description": description,
 | 
					        'description': description,
 | 
				
			||||||
        "email_address": emailAddress,
 | 
					        'email_address': emailAddress,
 | 
				
			||||||
        "power_level": powerLevel,
 | 
					        'power_level': powerLevel,
 | 
				
			||||||
        "external_id": externalId,
 | 
					        'external_id': externalId,
 | 
				
			||||||
      };
 | 
					      };
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -25,28 +25,28 @@ class Call {
 | 
				
			|||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  factory Call.fromJson(Map<String, dynamic> json) => Call(
 | 
					  factory Call.fromJson(Map<String, dynamic> json) => Call(
 | 
				
			||||||
        id: json["id"],
 | 
					        id: json['id'],
 | 
				
			||||||
        createdAt: DateTime.parse(json["created_at"]),
 | 
					        createdAt: DateTime.parse(json['created_at']),
 | 
				
			||||||
        updatedAt: DateTime.parse(json["updated_at"]),
 | 
					        updatedAt: DateTime.parse(json['updated_at']),
 | 
				
			||||||
        deletedAt: json["deleted_at"],
 | 
					        deletedAt: json['deleted_at'],
 | 
				
			||||||
        endedAt:
 | 
					        endedAt:
 | 
				
			||||||
            json["ended_at"] != null ? DateTime.parse(json["ended_at"]) : null,
 | 
					            json['ended_at'] != null ? DateTime.parse(json['ended_at']) : null,
 | 
				
			||||||
        externalId: json["external_id"],
 | 
					        externalId: json['external_id'],
 | 
				
			||||||
        founderId: json["founder_id"],
 | 
					        founderId: json['founder_id'],
 | 
				
			||||||
        channelId: json["channel_id"],
 | 
					        channelId: json['channel_id'],
 | 
				
			||||||
        channel: Channel.fromJson(json["channel"]),
 | 
					        channel: Channel.fromJson(json['channel']),
 | 
				
			||||||
      );
 | 
					      );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  Map<String, dynamic> toJson() => {
 | 
					  Map<String, dynamic> toJson() => {
 | 
				
			||||||
        "id": id,
 | 
					        'id': id,
 | 
				
			||||||
        "created_at": createdAt.toIso8601String(),
 | 
					        'created_at': createdAt.toIso8601String(),
 | 
				
			||||||
        "updated_at": updatedAt.toIso8601String(),
 | 
					        'updated_at': updatedAt.toIso8601String(),
 | 
				
			||||||
        "deleted_at": deletedAt,
 | 
					        'deleted_at': deletedAt,
 | 
				
			||||||
        "ended_at": endedAt?.toIso8601String(),
 | 
					        'ended_at': endedAt?.toIso8601String(),
 | 
				
			||||||
        "external_id": externalId,
 | 
					        'external_id': externalId,
 | 
				
			||||||
        "founder_id": founderId,
 | 
					        'founder_id': founderId,
 | 
				
			||||||
        "channel_id": channelId,
 | 
					        'channel_id': channelId,
 | 
				
			||||||
        "channel": channel.toJson(),
 | 
					        'channel': channel.toJson(),
 | 
				
			||||||
      };
 | 
					      };
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -32,35 +32,35 @@ class Channel {
 | 
				
			|||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  factory Channel.fromJson(Map<String, dynamic> json) => Channel(
 | 
					  factory Channel.fromJson(Map<String, dynamic> json) => Channel(
 | 
				
			||||||
        id: json["id"],
 | 
					        id: json['id'],
 | 
				
			||||||
        createdAt: DateTime.parse(json["created_at"]),
 | 
					        createdAt: DateTime.parse(json['created_at']),
 | 
				
			||||||
        updatedAt: DateTime.parse(json["updated_at"]),
 | 
					        updatedAt: DateTime.parse(json['updated_at']),
 | 
				
			||||||
        deletedAt: json["deleted_at"],
 | 
					        deletedAt: json['deleted_at'],
 | 
				
			||||||
        alias: json["alias"],
 | 
					        alias: json['alias'],
 | 
				
			||||||
        name: json["name"],
 | 
					        name: json['name'],
 | 
				
			||||||
        description: json["description"],
 | 
					        description: json['description'],
 | 
				
			||||||
        members: json["members"],
 | 
					        members: json['members'],
 | 
				
			||||||
        calls: json["calls"],
 | 
					        calls: json['calls'],
 | 
				
			||||||
        type: json["type"],
 | 
					        type: json['type'],
 | 
				
			||||||
        account: Account.fromJson(json["account"]),
 | 
					        account: Account.fromJson(json['account']),
 | 
				
			||||||
        accountId: json["account_id"],
 | 
					        accountId: json['account_id'],
 | 
				
			||||||
        realmId: json["realm_id"],
 | 
					        realmId: json['realm_id'],
 | 
				
			||||||
      );
 | 
					      );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  Map<String, dynamic> toJson() => {
 | 
					  Map<String, dynamic> toJson() => {
 | 
				
			||||||
        "id": id,
 | 
					        'id': id,
 | 
				
			||||||
        "created_at": createdAt.toIso8601String(),
 | 
					        'created_at': createdAt.toIso8601String(),
 | 
				
			||||||
        "updated_at": updatedAt.toIso8601String(),
 | 
					        'updated_at': updatedAt.toIso8601String(),
 | 
				
			||||||
        "deleted_at": deletedAt,
 | 
					        'deleted_at': deletedAt,
 | 
				
			||||||
        "alias": alias,
 | 
					        'alias': alias,
 | 
				
			||||||
        "name": name,
 | 
					        'name': name,
 | 
				
			||||||
        "description": description,
 | 
					        'description': description,
 | 
				
			||||||
        "members": members,
 | 
					        'members': members,
 | 
				
			||||||
        "calls": calls,
 | 
					        'calls': calls,
 | 
				
			||||||
        "type": type,
 | 
					        'type': type,
 | 
				
			||||||
        "account": account,
 | 
					        'account': account,
 | 
				
			||||||
        "account_id": accountId,
 | 
					        'account_id': accountId,
 | 
				
			||||||
        "realm_id": realmId,
 | 
					        'realm_id': realmId,
 | 
				
			||||||
      };
 | 
					      };
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -86,24 +86,24 @@ class ChannelMember {
 | 
				
			|||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  factory ChannelMember.fromJson(Map<String, dynamic> json) => ChannelMember(
 | 
					  factory ChannelMember.fromJson(Map<String, dynamic> json) => ChannelMember(
 | 
				
			||||||
        id: json["id"],
 | 
					        id: json['id'],
 | 
				
			||||||
        createdAt: DateTime.parse(json["created_at"]),
 | 
					        createdAt: DateTime.parse(json['created_at']),
 | 
				
			||||||
        updatedAt: DateTime.parse(json["updated_at"]),
 | 
					        updatedAt: DateTime.parse(json['updated_at']),
 | 
				
			||||||
        deletedAt: json["deleted_at"],
 | 
					        deletedAt: json['deleted_at'],
 | 
				
			||||||
        channelId: json["channel_id"],
 | 
					        channelId: json['channel_id'],
 | 
				
			||||||
        accountId: json["account_id"],
 | 
					        accountId: json['account_id'],
 | 
				
			||||||
        account: Account.fromJson(json["account"]),
 | 
					        account: Account.fromJson(json['account']),
 | 
				
			||||||
        notify: json["notify"],
 | 
					        notify: json['notify'],
 | 
				
			||||||
      );
 | 
					      );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  Map<String, dynamic> toJson() => {
 | 
					  Map<String, dynamic> toJson() => {
 | 
				
			||||||
        "id": id,
 | 
					        'id': id,
 | 
				
			||||||
        "created_at": createdAt.toIso8601String(),
 | 
					        'created_at': createdAt.toIso8601String(),
 | 
				
			||||||
        "updated_at": updatedAt.toIso8601String(),
 | 
					        'updated_at': updatedAt.toIso8601String(),
 | 
				
			||||||
        "deleted_at": deletedAt,
 | 
					        'deleted_at': deletedAt,
 | 
				
			||||||
        "channel_id": channelId,
 | 
					        'channel_id': channelId,
 | 
				
			||||||
        "account_id": accountId,
 | 
					        'account_id': accountId,
 | 
				
			||||||
        "account": account.toJson(),
 | 
					        'account': account.toJson(),
 | 
				
			||||||
        "notify": notify,
 | 
					        'notify': notify,
 | 
				
			||||||
      };
 | 
					      };
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -26,29 +26,29 @@ class Friendship {
 | 
				
			|||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  factory Friendship.fromJson(Map<String, dynamic> json) => Friendship(
 | 
					  factory Friendship.fromJson(Map<String, dynamic> json) => Friendship(
 | 
				
			||||||
        id: json["id"],
 | 
					        id: json['id'],
 | 
				
			||||||
        createdAt: DateTime.parse(json["created_at"]),
 | 
					        createdAt: DateTime.parse(json['created_at']),
 | 
				
			||||||
        updatedAt: DateTime.parse(json["updated_at"]),
 | 
					        updatedAt: DateTime.parse(json['updated_at']),
 | 
				
			||||||
        deletedAt: json["deleted_at"],
 | 
					        deletedAt: json['deleted_at'],
 | 
				
			||||||
        accountId: json["account_id"],
 | 
					        accountId: json['account_id'],
 | 
				
			||||||
        relatedId: json["related_id"],
 | 
					        relatedId: json['related_id'],
 | 
				
			||||||
        blockedBy: json["blocked_by"],
 | 
					        blockedBy: json['blocked_by'],
 | 
				
			||||||
        account: Account.fromJson(json["account"]),
 | 
					        account: Account.fromJson(json['account']),
 | 
				
			||||||
        related: Account.fromJson(json["related"]),
 | 
					        related: Account.fromJson(json['related']),
 | 
				
			||||||
        status: json["status"],
 | 
					        status: json['status'],
 | 
				
			||||||
      );
 | 
					      );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  Map<String, dynamic> toJson() => {
 | 
					  Map<String, dynamic> toJson() => {
 | 
				
			||||||
        "id": id,
 | 
					        'id': id,
 | 
				
			||||||
        "created_at": createdAt.toIso8601String(),
 | 
					        'created_at': createdAt.toIso8601String(),
 | 
				
			||||||
        "updated_at": updatedAt.toIso8601String(),
 | 
					        'updated_at': updatedAt.toIso8601String(),
 | 
				
			||||||
        "deleted_at": deletedAt,
 | 
					        'deleted_at': deletedAt,
 | 
				
			||||||
        "account_id": accountId,
 | 
					        'account_id': accountId,
 | 
				
			||||||
        "related_id": relatedId,
 | 
					        'related_id': relatedId,
 | 
				
			||||||
        "blocked_by": blockedBy,
 | 
					        'blocked_by': blockedBy,
 | 
				
			||||||
        "account": account.toJson(),
 | 
					        'account': account.toJson(),
 | 
				
			||||||
        "related": related.toJson(),
 | 
					        'related': related.toJson(),
 | 
				
			||||||
        "status": status,
 | 
					        'status': status,
 | 
				
			||||||
      };
 | 
					      };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  Account getOtherside(int selfId) {
 | 
					  Account getOtherside(int selfId) {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -36,42 +36,42 @@ class Message {
 | 
				
			|||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  factory Message.fromJson(Map<String, dynamic> json) => Message(
 | 
					  factory Message.fromJson(Map<String, dynamic> json) => Message(
 | 
				
			||||||
        id: json["id"],
 | 
					        id: json['id'],
 | 
				
			||||||
        createdAt: DateTime.parse(json["created_at"]),
 | 
					        createdAt: DateTime.parse(json['created_at']),
 | 
				
			||||||
        updatedAt: DateTime.parse(json["updated_at"]),
 | 
					        updatedAt: DateTime.parse(json['updated_at']),
 | 
				
			||||||
        deletedAt: json["deleted_at"],
 | 
					        deletedAt: json['deleted_at'],
 | 
				
			||||||
        content: json["content"],
 | 
					        content: json['content'],
 | 
				
			||||||
        metadata: json["metadata"],
 | 
					        metadata: json['metadata'],
 | 
				
			||||||
        type: json["type"],
 | 
					        type: json['type'],
 | 
				
			||||||
        attachments: List<Attachment>.from(
 | 
					        attachments: List<Attachment>.from(
 | 
				
			||||||
            json["attachments"]?.map((x) => Attachment.fromJson(x)) ??
 | 
					            json['attachments']?.map((x) => Attachment.fromJson(x)) ??
 | 
				
			||||||
                List.empty()),
 | 
					                List.empty()),
 | 
				
			||||||
        channel: Channel.fromJson(json["channel"]),
 | 
					        channel: Channel.fromJson(json['channel']),
 | 
				
			||||||
        sender: Sender.fromJson(json["sender"]),
 | 
					        sender: Sender.fromJson(json['sender']),
 | 
				
			||||||
        replyId: json["reply_id"],
 | 
					        replyId: json['reply_id'],
 | 
				
			||||||
        replyTo: json["reply_to"] != null
 | 
					        replyTo: json['reply_to'] != null
 | 
				
			||||||
            ? Message.fromJson(json["reply_to"])
 | 
					            ? Message.fromJson(json['reply_to'])
 | 
				
			||||||
            : null,
 | 
					            : null,
 | 
				
			||||||
        channelId: json["channel_id"],
 | 
					        channelId: json['channel_id'],
 | 
				
			||||||
        senderId: json["sender_id"],
 | 
					        senderId: json['sender_id'],
 | 
				
			||||||
      );
 | 
					      );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  Map<String, dynamic> toJson() => {
 | 
					  Map<String, dynamic> toJson() => {
 | 
				
			||||||
        "id": id,
 | 
					        'id': id,
 | 
				
			||||||
        "created_at": createdAt.toIso8601String(),
 | 
					        'created_at': createdAt.toIso8601String(),
 | 
				
			||||||
        "updated_at": updatedAt.toIso8601String(),
 | 
					        'updated_at': updatedAt.toIso8601String(),
 | 
				
			||||||
        "deleted_at": deletedAt,
 | 
					        'deleted_at': deletedAt,
 | 
				
			||||||
        "content": content,
 | 
					        'content': content,
 | 
				
			||||||
        "metadata": metadata,
 | 
					        'metadata': metadata,
 | 
				
			||||||
        "type": type,
 | 
					        'type': type,
 | 
				
			||||||
        "attachments": List<dynamic>.from(
 | 
					        'attachments': List<dynamic>.from(
 | 
				
			||||||
            attachments?.map((x) => x.toJson()) ?? List.empty()),
 | 
					            attachments?.map((x) => x.toJson()) ?? List.empty()),
 | 
				
			||||||
        "channel": channel?.toJson(),
 | 
					        'channel': channel?.toJson(),
 | 
				
			||||||
        "sender": sender.toJson(),
 | 
					        'sender': sender.toJson(),
 | 
				
			||||||
        "reply_id": replyId,
 | 
					        'reply_id': replyId,
 | 
				
			||||||
        "reply_to": replyTo?.toJson(),
 | 
					        'reply_to': replyTo?.toJson(),
 | 
				
			||||||
        "channel_id": channelId,
 | 
					        'channel_id': channelId,
 | 
				
			||||||
        "sender_id": senderId,
 | 
					        'sender_id': senderId,
 | 
				
			||||||
      };
 | 
					      };
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -97,24 +97,24 @@ class Sender {
 | 
				
			|||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  factory Sender.fromJson(Map<String, dynamic> json) => Sender(
 | 
					  factory Sender.fromJson(Map<String, dynamic> json) => Sender(
 | 
				
			||||||
        id: json["id"],
 | 
					        id: json['id'],
 | 
				
			||||||
        createdAt: DateTime.parse(json["created_at"]),
 | 
					        createdAt: DateTime.parse(json['created_at']),
 | 
				
			||||||
        updatedAt: DateTime.parse(json["updated_at"]),
 | 
					        updatedAt: DateTime.parse(json['updated_at']),
 | 
				
			||||||
        deletedAt: json["deleted_at"],
 | 
					        deletedAt: json['deleted_at'],
 | 
				
			||||||
        account: Account.fromJson(json["account"]),
 | 
					        account: Account.fromJson(json['account']),
 | 
				
			||||||
        channelId: json["channel_id"],
 | 
					        channelId: json['channel_id'],
 | 
				
			||||||
        accountId: json["account_id"],
 | 
					        accountId: json['account_id'],
 | 
				
			||||||
        notify: json["notify"],
 | 
					        notify: json['notify'],
 | 
				
			||||||
      );
 | 
					      );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  Map<String, dynamic> toJson() => {
 | 
					  Map<String, dynamic> toJson() => {
 | 
				
			||||||
        "id": id,
 | 
					        'id': id,
 | 
				
			||||||
        "created_at": createdAt.toIso8601String(),
 | 
					        'created_at': createdAt.toIso8601String(),
 | 
				
			||||||
        "updated_at": updatedAt.toIso8601String(),
 | 
					        'updated_at': updatedAt.toIso8601String(),
 | 
				
			||||||
        "deleted_at": deletedAt,
 | 
					        'deleted_at': deletedAt,
 | 
				
			||||||
        "account": account.toJson(),
 | 
					        'account': account.toJson(),
 | 
				
			||||||
        "channel_id": channelId,
 | 
					        'channel_id': channelId,
 | 
				
			||||||
        "account_id": accountId,
 | 
					        'account_id': accountId,
 | 
				
			||||||
        "notify": notify,
 | 
					        'notify': notify,
 | 
				
			||||||
      };
 | 
					      };
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -28,37 +28,37 @@ class Notification {
 | 
				
			|||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  factory Notification.fromJson(Map<String, dynamic> json) => Notification(
 | 
					  factory Notification.fromJson(Map<String, dynamic> json) => Notification(
 | 
				
			||||||
        id: json["id"],
 | 
					        id: json['id'],
 | 
				
			||||||
        createdAt: DateTime.parse(json["created_at"]),
 | 
					        createdAt: DateTime.parse(json['created_at']),
 | 
				
			||||||
        updatedAt: DateTime.parse(json["updated_at"]),
 | 
					        updatedAt: DateTime.parse(json['updated_at']),
 | 
				
			||||||
        deletedAt: json["deleted_at"],
 | 
					        deletedAt: json['deleted_at'],
 | 
				
			||||||
        subject: json["subject"],
 | 
					        subject: json['subject'],
 | 
				
			||||||
        content: json["content"],
 | 
					        content: json['content'],
 | 
				
			||||||
        links: json["links"] != null
 | 
					        links: json['links'] != null
 | 
				
			||||||
            ? List<Link>.from(json["links"].map((x) => Link.fromJson(x)))
 | 
					            ? List<Link>.from(json['links'].map((x) => Link.fromJson(x)))
 | 
				
			||||||
            : List.empty(),
 | 
					            : List.empty(),
 | 
				
			||||||
        isImportant: json["is_important"],
 | 
					        isImportant: json['is_important'],
 | 
				
			||||||
        isRealtime: json["is_realtime"],
 | 
					        isRealtime: json['is_realtime'],
 | 
				
			||||||
        readAt: json["read_at"],
 | 
					        readAt: json['read_at'],
 | 
				
			||||||
        senderId: json["sender_id"],
 | 
					        senderId: json['sender_id'],
 | 
				
			||||||
        recipientId: json["recipient_id"],
 | 
					        recipientId: json['recipient_id'],
 | 
				
			||||||
      );
 | 
					      );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  Map<String, dynamic> toJson() => {
 | 
					  Map<String, dynamic> toJson() => {
 | 
				
			||||||
        "id": id,
 | 
					        'id': id,
 | 
				
			||||||
        "created_at": createdAt.toIso8601String(),
 | 
					        'created_at': createdAt.toIso8601String(),
 | 
				
			||||||
        "updated_at": updatedAt.toIso8601String(),
 | 
					        'updated_at': updatedAt.toIso8601String(),
 | 
				
			||||||
        "deleted_at": deletedAt,
 | 
					        'deleted_at': deletedAt,
 | 
				
			||||||
        "subject": subject,
 | 
					        'subject': subject,
 | 
				
			||||||
        "content": content,
 | 
					        'content': content,
 | 
				
			||||||
        "links": links != null
 | 
					        'links': links != null
 | 
				
			||||||
            ? List<dynamic>.from(links!.map((x) => x.toJson()))
 | 
					            ? List<dynamic>.from(links!.map((x) => x.toJson()))
 | 
				
			||||||
            : List.empty(),
 | 
					            : List.empty(),
 | 
				
			||||||
        "is_important": isImportant,
 | 
					        'is_important': isImportant,
 | 
				
			||||||
        "is_realtime": isRealtime,
 | 
					        'is_realtime': isRealtime,
 | 
				
			||||||
        "read_at": readAt,
 | 
					        'read_at': readAt,
 | 
				
			||||||
        "sender_id": senderId,
 | 
					        'sender_id': senderId,
 | 
				
			||||||
        "recipient_id": recipientId,
 | 
					        'recipient_id': recipientId,
 | 
				
			||||||
      };
 | 
					      };
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -72,12 +72,12 @@ class Link {
 | 
				
			|||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  factory Link.fromJson(Map<String, dynamic> json) => Link(
 | 
					  factory Link.fromJson(Map<String, dynamic> json) => Link(
 | 
				
			||||||
        label: json["label"],
 | 
					        label: json['label'],
 | 
				
			||||||
        url: json["url"],
 | 
					        url: json['url'],
 | 
				
			||||||
      );
 | 
					      );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  Map<String, dynamic> toJson() => {
 | 
					  Map<String, dynamic> toJson() => {
 | 
				
			||||||
        "label": label,
 | 
					        'label': label,
 | 
				
			||||||
        "url": url,
 | 
					        'url': url,
 | 
				
			||||||
      };
 | 
					      };
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -10,14 +10,14 @@ class NetworkPackage {
 | 
				
			|||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  factory NetworkPackage.fromJson(Map<String, dynamic> json) => NetworkPackage(
 | 
					  factory NetworkPackage.fromJson(Map<String, dynamic> json) => NetworkPackage(
 | 
				
			||||||
        method: json["w"],
 | 
					        method: json['w'],
 | 
				
			||||||
        message: json["m"],
 | 
					        message: json['m'],
 | 
				
			||||||
        payload: json["p"],
 | 
					        payload: json['p'],
 | 
				
			||||||
      );
 | 
					      );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  Map<String, dynamic> toJson() => {
 | 
					  Map<String, dynamic> toJson() => {
 | 
				
			||||||
        "w": method,
 | 
					        'w': method,
 | 
				
			||||||
        "m": message,
 | 
					        'm': message,
 | 
				
			||||||
        "p": payload,
 | 
					        'p': payload,
 | 
				
			||||||
      };
 | 
					      };
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -8,10 +8,10 @@ class PaginationResult {
 | 
				
			|||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  factory PaginationResult.fromJson(Map<String, dynamic> json) =>
 | 
					  factory PaginationResult.fromJson(Map<String, dynamic> json) =>
 | 
				
			||||||
      PaginationResult(count: json["count"], data: json["data"]);
 | 
					      PaginationResult(count: json['count'], data: json['data']);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  Map<String, dynamic> toJson() => {
 | 
					  Map<String, dynamic> toJson() => {
 | 
				
			||||||
        "count": count,
 | 
					        'count': count,
 | 
				
			||||||
        "data": data,
 | 
					        'data': data,
 | 
				
			||||||
      };
 | 
					      };
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -18,6 +18,8 @@ class Post {
 | 
				
			|||||||
  List<Attachment>? attachments;
 | 
					  List<Attachment>? attachments;
 | 
				
			||||||
  Map<String, dynamic>? reactionList;
 | 
					  Map<String, dynamic>? reactionList;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  String get dataset => '${modelType}s';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  Post({
 | 
					  Post({
 | 
				
			||||||
    required this.id,
 | 
					    required this.id,
 | 
				
			||||||
    required this.createdAt,
 | 
					    required this.createdAt,
 | 
				
			||||||
@@ -38,46 +40,46 @@ class Post {
 | 
				
			|||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  factory Post.fromJson(Map<String, dynamic> json) => Post(
 | 
					  factory Post.fromJson(Map<String, dynamic> json) => Post(
 | 
				
			||||||
        id: json["id"],
 | 
					        id: json['id'],
 | 
				
			||||||
        createdAt: DateTime.parse(json["created_at"]),
 | 
					        createdAt: DateTime.parse(json['created_at']),
 | 
				
			||||||
        updatedAt: DateTime.parse(json["updated_at"]),
 | 
					        updatedAt: DateTime.parse(json['updated_at']),
 | 
				
			||||||
        deletedAt: json["deleted_at"],
 | 
					        deletedAt: json['deleted_at'],
 | 
				
			||||||
        alias: json["alias"],
 | 
					        alias: json['alias'],
 | 
				
			||||||
        title: json["title"],
 | 
					        title: json['title'],
 | 
				
			||||||
        description: json["description"],
 | 
					        description: json['description'],
 | 
				
			||||||
        content: json["content"],
 | 
					        content: json['content'],
 | 
				
			||||||
        modelType: json["model_type"],
 | 
					        modelType: json['model_type'],
 | 
				
			||||||
        commentCount: json["comment_count"],
 | 
					        commentCount: json['comment_count'],
 | 
				
			||||||
        reactionCount: json["reaction_count"],
 | 
					        reactionCount: json['reaction_count'],
 | 
				
			||||||
        authorId: json["author_id"],
 | 
					        authorId: json['author_id'],
 | 
				
			||||||
        realmId: json["realm_id"],
 | 
					        realmId: json['realm_id'],
 | 
				
			||||||
        author: Author.fromJson(json["author"]),
 | 
					        author: Author.fromJson(json['author']),
 | 
				
			||||||
        attachments: json["attachments"] != null
 | 
					        attachments: json['attachments'] != null
 | 
				
			||||||
            ? List<Attachment>.from(
 | 
					            ? List<Attachment>.from(
 | 
				
			||||||
                json["attachments"].map((x) => Attachment.fromJson(x)))
 | 
					                json['attachments'].map((x) => Attachment.fromJson(x)))
 | 
				
			||||||
            : List.empty(),
 | 
					            : List.empty(),
 | 
				
			||||||
        reactionList: json["reaction_list"],
 | 
					        reactionList: json['reaction_list'],
 | 
				
			||||||
      );
 | 
					      );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  Map<String, dynamic> toJson() => {
 | 
					  Map<String, dynamic> toJson() => {
 | 
				
			||||||
        "id": id,
 | 
					        'id': id,
 | 
				
			||||||
        "created_at": createdAt.toIso8601String(),
 | 
					        'created_at': createdAt.toIso8601String(),
 | 
				
			||||||
        "updated_at": updatedAt.toIso8601String(),
 | 
					        'updated_at': updatedAt.toIso8601String(),
 | 
				
			||||||
        "deleted_at": deletedAt,
 | 
					        'deleted_at': deletedAt,
 | 
				
			||||||
        "alias": alias,
 | 
					        'alias': alias,
 | 
				
			||||||
        "title": title,
 | 
					        'title': title,
 | 
				
			||||||
        "description": description,
 | 
					        'description': description,
 | 
				
			||||||
        "content": content,
 | 
					        'content': content,
 | 
				
			||||||
        "model_type": modelType,
 | 
					        'model_type': modelType,
 | 
				
			||||||
        "comment_count": commentCount,
 | 
					        'comment_count': commentCount,
 | 
				
			||||||
        "reaction_count": reactionCount,
 | 
					        'reaction_count': reactionCount,
 | 
				
			||||||
        "author_id": authorId,
 | 
					        'author_id': authorId,
 | 
				
			||||||
        "realm_id": realmId,
 | 
					        'realm_id': realmId,
 | 
				
			||||||
        "author": author.toJson(),
 | 
					        'author': author.toJson(),
 | 
				
			||||||
        "attachments": attachments == null
 | 
					        'attachments': attachments == null
 | 
				
			||||||
            ? List.empty()
 | 
					            ? List.empty()
 | 
				
			||||||
            : List<dynamic>.from(attachments!.map((x) => x.toJson())),
 | 
					            : List<dynamic>.from(attachments!.map((x) => x.toJson())),
 | 
				
			||||||
        "reaction_list": reactionList,
 | 
					        'reaction_list': reactionList,
 | 
				
			||||||
      };
 | 
					      };
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -117,38 +119,38 @@ class Attachment {
 | 
				
			|||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  factory Attachment.fromJson(Map<String, dynamic> json) => Attachment(
 | 
					  factory Attachment.fromJson(Map<String, dynamic> json) => Attachment(
 | 
				
			||||||
        id: json["id"],
 | 
					        id: json['id'],
 | 
				
			||||||
        createdAt: DateTime.parse(json["created_at"]),
 | 
					        createdAt: DateTime.parse(json['created_at']),
 | 
				
			||||||
        updatedAt: DateTime.parse(json["updated_at"]),
 | 
					        updatedAt: DateTime.parse(json['updated_at']),
 | 
				
			||||||
        deletedAt: json["deleted_at"],
 | 
					        deletedAt: json['deleted_at'],
 | 
				
			||||||
        fileId: json["file_id"],
 | 
					        fileId: json['file_id'],
 | 
				
			||||||
        filesize: json["filesize"],
 | 
					        filesize: json['filesize'],
 | 
				
			||||||
        filename: json["filename"],
 | 
					        filename: json['filename'],
 | 
				
			||||||
        mimetype: json["mimetype"],
 | 
					        mimetype: json['mimetype'],
 | 
				
			||||||
        type: json["type"],
 | 
					        type: json['type'],
 | 
				
			||||||
        externalUrl: json["external_url"],
 | 
					        externalUrl: json['external_url'],
 | 
				
			||||||
        author: Author.fromJson(json["author"]),
 | 
					        author: Author.fromJson(json['author']),
 | 
				
			||||||
        articleId: json["article_id"],
 | 
					        articleId: json['article_id'],
 | 
				
			||||||
        momentId: json["moment_id"],
 | 
					        momentId: json['moment_id'],
 | 
				
			||||||
        commentId: json["comment_id"],
 | 
					        commentId: json['comment_id'],
 | 
				
			||||||
        authorId: json["author_id"],
 | 
					        authorId: json['author_id'],
 | 
				
			||||||
      );
 | 
					      );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  Map<String, dynamic> toJson() => {
 | 
					  Map<String, dynamic> toJson() => {
 | 
				
			||||||
        "id": id,
 | 
					        'id': id,
 | 
				
			||||||
        "created_at": createdAt.toIso8601String(),
 | 
					        'created_at': createdAt.toIso8601String(),
 | 
				
			||||||
        "updated_at": updatedAt.toIso8601String(),
 | 
					        'updated_at': updatedAt.toIso8601String(),
 | 
				
			||||||
        "deleted_at": deletedAt,
 | 
					        'deleted_at': deletedAt,
 | 
				
			||||||
        "file_id": fileId,
 | 
					        'file_id': fileId,
 | 
				
			||||||
        "filesize": filesize,
 | 
					        'filesize': filesize,
 | 
				
			||||||
        "filename": filename,
 | 
					        'filename': filename,
 | 
				
			||||||
        "mimetype": mimetype,
 | 
					        'mimetype': mimetype,
 | 
				
			||||||
        "type": type,
 | 
					        'type': type,
 | 
				
			||||||
        "external_url": externalUrl,
 | 
					        'external_url': externalUrl,
 | 
				
			||||||
        "author": author.toJson(),
 | 
					        'author': author.toJson(),
 | 
				
			||||||
        "article_id": articleId,
 | 
					        'article_id': articleId,
 | 
				
			||||||
        "moment_id": momentId,
 | 
					        'moment_id': momentId,
 | 
				
			||||||
        "comment_id": commentId,
 | 
					        'comment_id': commentId,
 | 
				
			||||||
        "author_id": authorId,
 | 
					        'author_id': authorId,
 | 
				
			||||||
      };
 | 
					      };
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -14,12 +14,12 @@ class AuthProvider extends ChangeNotifier {
 | 
				
			|||||||
  final userinfoEndpoint = getRequestUri('passport', '/api/users/me');
 | 
					  final userinfoEndpoint = getRequestUri('passport', '/api/users/me');
 | 
				
			||||||
  final redirectUrl = Uri.parse('solian://auth');
 | 
					  final redirectUrl = Uri.parse('solian://auth');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  static const clientId = "solian";
 | 
					  static const clientId = 'solian';
 | 
				
			||||||
  static const clientSecret = "_F4%q2Eea3";
 | 
					  static const clientSecret = '_F4%q2Eea3';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  static const storage = FlutterSecureStorage();
 | 
					  static const storage = FlutterSecureStorage();
 | 
				
			||||||
  static const storageKey = "identity";
 | 
					  static const storageKey = 'identity';
 | 
				
			||||||
  static const profileKey = "profiles";
 | 
					  static const profileKey = 'profiles';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  /// Before use this variable to make request
 | 
					  /// Before use this variable to make request
 | 
				
			||||||
  /// **MAKE SURE YOU HAVE CALL THE isAuthorized() METHOD**
 | 
					  /// **MAKE SURE YOU HAVE CALL THE isAuthorized() METHOD**
 | 
				
			||||||
@@ -57,7 +57,7 @@ class AuthProvider extends ChangeNotifier {
 | 
				
			|||||||
      password,
 | 
					      password,
 | 
				
			||||||
      identifier: clientId,
 | 
					      identifier: clientId,
 | 
				
			||||||
      secret: clientSecret,
 | 
					      secret: clientSecret,
 | 
				
			||||||
      scopes: ["openid"],
 | 
					      scopes: ['openid'],
 | 
				
			||||||
      basicAuth: false,
 | 
					      basicAuth: false,
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
@@ -115,6 +115,6 @@ class AuthProvider extends ChangeNotifier {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  Future<dynamic> getProfiles() async {
 | 
					  Future<dynamic> getProfiles() async {
 | 
				
			||||||
    const storage = FlutterSecureStorage();
 | 
					    const storage = FlutterSecureStorage();
 | 
				
			||||||
    return jsonDecode(await storage.read(key: profileKey) ?? "{}");
 | 
					    return jsonDecode(await storage.read(key: profileKey) ?? '{}');
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -2,6 +2,7 @@ import 'dart:async';
 | 
				
			|||||||
import 'dart:convert';
 | 
					import 'dart:convert';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import 'package:flutter/material.dart';
 | 
					import 'package:flutter/material.dart';
 | 
				
			||||||
 | 
					import 'package:http/http.dart';
 | 
				
			||||||
import 'package:livekit_client/livekit_client.dart';
 | 
					import 'package:livekit_client/livekit_client.dart';
 | 
				
			||||||
import 'package:permission_handler/permission_handler.dart';
 | 
					import 'package:permission_handler/permission_handler.dart';
 | 
				
			||||||
import 'package:provider/provider.dart';
 | 
					import 'package:provider/provider.dart';
 | 
				
			||||||
@@ -17,9 +18,11 @@ import 'package:flutter_gen/gen_l10n/app_localizations.dart';
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
class ChatProvider extends ChangeNotifier {
 | 
					class ChatProvider extends ChangeNotifier {
 | 
				
			||||||
  bool isOpened = false;
 | 
					  bool isOpened = false;
 | 
				
			||||||
  bool isShown = false;
 | 
					  bool isCallShown = false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  ChatCallInstance? call;
 | 
					  Call? ongoingCall;
 | 
				
			||||||
 | 
					  Channel? focusChannel;
 | 
				
			||||||
 | 
					  ChatCallInstance? currentCall;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  Future<WebSocketChannel?> connect(AuthProvider auth) async {
 | 
					  Future<WebSocketChannel?> connect(AuthProvider auth) async {
 | 
				
			||||||
    if (auth.client == null) await auth.loadClient();
 | 
					    if (auth.client == null) await auth.loadClient();
 | 
				
			||||||
@@ -43,17 +46,51 @@ class ChatProvider extends ChangeNotifier {
 | 
				
			|||||||
    return channel;
 | 
					    return channel;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  bool handleCall(Call call, Channel channel,
 | 
					  Future<Channel> fetchChannel(String alias) async {
 | 
				
			||||||
      {Function? onUpdate, Function? onDispose}) {
 | 
					    final Client client = Client();
 | 
				
			||||||
    if (this.call != null) return false;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    this.call = ChatCallInstance(
 | 
					    var uri = getRequestUri('messaging', '/api/channels/$alias');
 | 
				
			||||||
 | 
					    var res = await client.get(uri);
 | 
				
			||||||
 | 
					    if (res.statusCode == 200) {
 | 
				
			||||||
 | 
					      final result = jsonDecode(utf8.decode(res.bodyBytes));
 | 
				
			||||||
 | 
					      focusChannel = Channel.fromJson(result);
 | 
				
			||||||
 | 
					      notifyListeners();
 | 
				
			||||||
 | 
					      return focusChannel!;
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      var message = utf8.decode(res.bodyBytes);
 | 
				
			||||||
 | 
					      throw Exception(message);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  Future<Call?> fetchOngoingCall(String alias) async {
 | 
				
			||||||
 | 
					    final Client client = Client();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    var uri = getRequestUri('messaging', '/api/channels/$alias/calls/ongoing');
 | 
				
			||||||
 | 
					    var res = await client.get(uri);
 | 
				
			||||||
 | 
					    if (res.statusCode == 200) {
 | 
				
			||||||
 | 
					      final result = jsonDecode(utf8.decode(res.bodyBytes));
 | 
				
			||||||
 | 
					      ongoingCall = Call.fromJson(result);
 | 
				
			||||||
 | 
					      notifyListeners();
 | 
				
			||||||
 | 
					      return ongoingCall;
 | 
				
			||||||
 | 
					    } else if (res.statusCode != 404) {
 | 
				
			||||||
 | 
					      var message = utf8.decode(res.bodyBytes);
 | 
				
			||||||
 | 
					      throw Exception(message);
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      return null;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  bool handleCallJoin(Call call, Channel channel,
 | 
				
			||||||
 | 
					      {Function? onUpdate, Function? onDispose}) {
 | 
				
			||||||
 | 
					    if (currentCall != null) return false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    currentCall = ChatCallInstance(
 | 
				
			||||||
      onUpdate: () {
 | 
					      onUpdate: () {
 | 
				
			||||||
        notifyListeners();
 | 
					        notifyListeners();
 | 
				
			||||||
        if (onUpdate != null) onUpdate();
 | 
					        if (onUpdate != null) onUpdate();
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
      onDispose: () {
 | 
					      onDispose: () {
 | 
				
			||||||
        this.call = null;
 | 
					        currentCall = null;
 | 
				
			||||||
        notifyListeners();
 | 
					        notifyListeners();
 | 
				
			||||||
        if (onDispose != null) onDispose();
 | 
					        if (onDispose != null) onDispose();
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
@@ -64,8 +101,13 @@ class ChatProvider extends ChangeNotifier {
 | 
				
			|||||||
    return true;
 | 
					    return true;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  void setShown(bool state) {
 | 
					  void setOngoingCall(Call? item) {
 | 
				
			||||||
    isShown = state;
 | 
					    ongoingCall = item;
 | 
				
			||||||
 | 
					    notifyListeners();
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  void setCallShown(bool state) {
 | 
				
			||||||
 | 
					    isCallShown = state;
 | 
				
			||||||
    notifyListeners();
 | 
					    notifyListeners();
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@@ -118,8 +160,9 @@ class ChatCallInstance {
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  Future<void> checkPermissions() async {
 | 
					  Future<void> checkPermissions() async {
 | 
				
			||||||
    if (lkPlatformIs(PlatformType.macOS) || lkPlatformIs(PlatformType.linux))
 | 
					    if (lkPlatformIs(PlatformType.macOS) || lkPlatformIs(PlatformType.linux)) {
 | 
				
			||||||
      return;
 | 
					      return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    await Permission.camera.request();
 | 
					    await Permission.camera.request();
 | 
				
			||||||
    await Permission.microphone.request();
 | 
					    await Permission.microphone.request();
 | 
				
			||||||
@@ -133,7 +176,7 @@ class ChatCallInstance {
 | 
				
			|||||||
    final auth = context.read<AuthProvider>();
 | 
					    final auth = context.read<AuthProvider>();
 | 
				
			||||||
    if (!await auth.isAuthorized()) {
 | 
					    if (!await auth.isAuthorized()) {
 | 
				
			||||||
      onDispose();
 | 
					      onDispose();
 | 
				
			||||||
      throw Exception("unauthorized");
 | 
					      throw Exception('unauthorized');
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    var uri = getRequestUri(
 | 
					    var uri = getRequestUri(
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -29,7 +29,7 @@ class NotifyProvider extends ChangeNotifier {
 | 
				
			|||||||
    const androidSettings = AndroidInitializationSettings('app_icon');
 | 
					    const androidSettings = AndroidInitializationSettings('app_icon');
 | 
				
			||||||
    const darwinSettings = DarwinInitializationSettings(
 | 
					    const darwinSettings = DarwinInitializationSettings(
 | 
				
			||||||
      notificationCategories: [
 | 
					      notificationCategories: [
 | 
				
			||||||
        DarwinNotificationCategory("general"),
 | 
					        DarwinNotificationCategory('general'),
 | 
				
			||||||
      ],
 | 
					      ],
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
    const linuxSettings =
 | 
					    const linuxSettings =
 | 
				
			||||||
@@ -46,8 +46,9 @@ class NotifyProvider extends ChangeNotifier {
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  Future<void> requestPermissions() async {
 | 
					  Future<void> requestPermissions() async {
 | 
				
			||||||
    if (lkPlatformIs(PlatformType.macOS) || lkPlatformIs(PlatformType.linux))
 | 
					    if (lkPlatformIs(PlatformType.macOS) || lkPlatformIs(PlatformType.linux)) {
 | 
				
			||||||
      return;
 | 
					      return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
    await Permission.notification.request();
 | 
					    await Permission.notification.request();
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -97,7 +97,7 @@ class NameCard extends StatelessWidget {
 | 
				
			|||||||
  Future<Widget> renderAvatar(BuildContext context) async {
 | 
					  Future<Widget> renderAvatar(BuildContext context) async {
 | 
				
			||||||
    final auth = context.read<AuthProvider>();
 | 
					    final auth = context.read<AuthProvider>();
 | 
				
			||||||
    final profiles = await auth.getProfiles();
 | 
					    final profiles = await auth.getProfiles();
 | 
				
			||||||
    return AccountAvatar(source: profiles["picture"], direct: true);
 | 
					    return AccountAvatar(source: profiles['picture'], direct: true);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  Future<Column> renderLabel(BuildContext context) async {
 | 
					  Future<Column> renderLabel(BuildContext context) async {
 | 
				
			||||||
@@ -107,13 +107,13 @@ class NameCard extends StatelessWidget {
 | 
				
			|||||||
      crossAxisAlignment: CrossAxisAlignment.start,
 | 
					      crossAxisAlignment: CrossAxisAlignment.start,
 | 
				
			||||||
      children: [
 | 
					      children: [
 | 
				
			||||||
        Text(
 | 
					        Text(
 | 
				
			||||||
          profiles["nick"],
 | 
					          profiles['nick'],
 | 
				
			||||||
          style: const TextStyle(
 | 
					          style: const TextStyle(
 | 
				
			||||||
            fontSize: 20,
 | 
					            fontSize: 20,
 | 
				
			||||||
            fontWeight: FontWeight.bold,
 | 
					            fontWeight: FontWeight.bold,
 | 
				
			||||||
          ),
 | 
					          ),
 | 
				
			||||||
        ),
 | 
					        ),
 | 
				
			||||||
        Text(profiles["email"])
 | 
					        Text(profiles['email'])
 | 
				
			||||||
      ],
 | 
					      ],
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -157,8 +157,9 @@ class _FriendScreenState extends State<FriendScreen> {
 | 
				
			|||||||
  DismissDirection getDismissDirection(Friendship relation) {
 | 
					  DismissDirection getDismissDirection(Friendship relation) {
 | 
				
			||||||
    if (relation.status == 2) return DismissDirection.endToStart;
 | 
					    if (relation.status == 2) return DismissDirection.endToStart;
 | 
				
			||||||
    if (relation.status == 1) return DismissDirection.startToEnd;
 | 
					    if (relation.status == 1) return DismissDirection.startToEnd;
 | 
				
			||||||
    if (relation.status == 0 && relation.relatedId != _selfId)
 | 
					    if (relation.status == 0 && relation.relatedId != _selfId) {
 | 
				
			||||||
      return DismissDirection.startToEnd;
 | 
					      return DismissDirection.startToEnd;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
    return DismissDirection.horizontal;
 | 
					    return DismissDirection.horizontal;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,41 +0,0 @@
 | 
				
			|||||||
import 'package:flutter/material.dart';
 | 
					 | 
				
			||||||
import 'package:url_launcher/url_launcher.dart';
 | 
					 | 
				
			||||||
import 'package:webview_flutter/webview_flutter.dart';
 | 
					 | 
				
			||||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
class AuthorizationScreen extends StatelessWidget {
 | 
					 | 
				
			||||||
  final Uri authorizationUrl;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  const AuthorizationScreen(this.authorizationUrl, {super.key});
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  @override
 | 
					 | 
				
			||||||
  Widget build(BuildContext context) {
 | 
					 | 
				
			||||||
    return Scaffold(
 | 
					 | 
				
			||||||
      appBar: AppBar(
 | 
					 | 
				
			||||||
        title: Text(AppLocalizations.of(context)!.signIn),
 | 
					 | 
				
			||||||
      ),
 | 
					 | 
				
			||||||
      body: Stack(children: [
 | 
					 | 
				
			||||||
        WebViewWidget(
 | 
					 | 
				
			||||||
          controller: WebViewController()
 | 
					 | 
				
			||||||
            ..setJavaScriptMode(JavaScriptMode.unrestricted)
 | 
					 | 
				
			||||||
            ..setBackgroundColor(Colors.white)
 | 
					 | 
				
			||||||
            ..setNavigationDelegate(NavigationDelegate(
 | 
					 | 
				
			||||||
              onNavigationRequest: (NavigationRequest request) {
 | 
					 | 
				
			||||||
                if (request.url.startsWith('solian')) {
 | 
					 | 
				
			||||||
                  Navigator.of(context).pop(request.url);
 | 
					 | 
				
			||||||
                  WebViewCookieManager().clearCookies();
 | 
					 | 
				
			||||||
                  return NavigationDecision.prevent;
 | 
					 | 
				
			||||||
                } else if (request.url.contains("sign-up")) {
 | 
					 | 
				
			||||||
                  launchUrl(Uri.parse(request.url));
 | 
					 | 
				
			||||||
                  return NavigationDecision.prevent;
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
                return NavigationDecision.navigate;
 | 
					 | 
				
			||||||
              },
 | 
					 | 
				
			||||||
            ))
 | 
					 | 
				
			||||||
            ..loadRequest(authorizationUrl)
 | 
					 | 
				
			||||||
            ..clearCache(),
 | 
					 | 
				
			||||||
        ),
 | 
					 | 
				
			||||||
      ]),
 | 
					 | 
				
			||||||
    );
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@@ -24,14 +24,14 @@ class _ChatCallState extends State<ChatCall> {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  late ChatProvider _chat;
 | 
					  late ChatProvider _chat;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  ChatCallInstance get _call => _chat.call!;
 | 
					  ChatCallInstance get _call => _chat.currentCall!;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  void initState() {
 | 
					  void initState() {
 | 
				
			||||||
    super.initState();
 | 
					    super.initState();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    WidgetsBinding.instance.addPostFrameCallback((_) {
 | 
					    WidgetsBinding.instance.addPostFrameCallback((_) {
 | 
				
			||||||
      _chat.setShown(true);
 | 
					      _chat.setCallShown(true);
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -40,13 +40,13 @@ class _ChatCallState extends State<ChatCall> {
 | 
				
			|||||||
    _chat = context.watch<ChatProvider>();
 | 
					    _chat = context.watch<ChatProvider>();
 | 
				
			||||||
    if (!_isHandled) {
 | 
					    if (!_isHandled) {
 | 
				
			||||||
      _isHandled = true;
 | 
					      _isHandled = true;
 | 
				
			||||||
      if (_chat.handleCall(widget.call, widget.call.channel)) {
 | 
					      if (_chat.handleCallJoin(widget.call, widget.call.channel)) {
 | 
				
			||||||
        _chat.call?.init();
 | 
					        _chat.currentCall?.init();
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    Widget content;
 | 
					    Widget content;
 | 
				
			||||||
    if (_chat.call == null) {
 | 
					    if (_chat.currentCall == null) {
 | 
				
			||||||
      content = const Center(
 | 
					      content = const Center(
 | 
				
			||||||
        child: CircularProgressIndicator(),
 | 
					        child: CircularProgressIndicator(),
 | 
				
			||||||
      );
 | 
					      );
 | 
				
			||||||
@@ -136,7 +136,7 @@ class _ChatCallState extends State<ChatCall> {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  void deactivate() {
 | 
					  void deactivate() {
 | 
				
			||||||
    WidgetsBinding.instance.addPostFrameCallback((_) => _chat.setShown(false));
 | 
					    WidgetsBinding.instance.addPostFrameCallback((_) => _chat.setCallShown(false));
 | 
				
			||||||
    super.deactivate();
 | 
					    super.deactivate();
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -42,7 +42,7 @@ class _ChannelEditorScreenState extends State<ChannelEditorScreen> {
 | 
				
			|||||||
        ? getRequestUri('messaging', '/api/channels')
 | 
					        ? getRequestUri('messaging', '/api/channels')
 | 
				
			||||||
        : getRequestUri('messaging', '/api/channels/${widget.editing!.id}');
 | 
					        : getRequestUri('messaging', '/api/channels/${widget.editing!.id}');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    final req = Request(widget.editing == null ? "POST" : "PUT", uri);
 | 
					    final req = Request(widget.editing == null ? 'POST' : 'PUT', uri);
 | 
				
			||||||
    req.headers['Content-Type'] = 'application/json';
 | 
					    req.headers['Content-Type'] = 'application/json';
 | 
				
			||||||
    req.body = jsonEncode(<String, dynamic>{
 | 
					    req.body = jsonEncode(<String, dynamic>{
 | 
				
			||||||
      'alias': _aliasController.value.text.toLowerCase(),
 | 
					      'alias': _aliasController.value.text.toLowerCase(),
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -4,69 +4,52 @@ import 'package:flutter/material.dart';
 | 
				
			|||||||
import 'package:flutter_animate/flutter_animate.dart';
 | 
					import 'package:flutter_animate/flutter_animate.dart';
 | 
				
			||||||
import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
 | 
					import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
 | 
				
			||||||
import 'package:provider/provider.dart';
 | 
					import 'package:provider/provider.dart';
 | 
				
			||||||
import 'package:solian/models/call.dart';
 | 
					 | 
				
			||||||
import 'package:solian/models/channel.dart';
 | 
					 | 
				
			||||||
import 'package:solian/models/message.dart';
 | 
					import 'package:solian/models/message.dart';
 | 
				
			||||||
import 'package:solian/models/pagination.dart';
 | 
					import 'package:solian/models/pagination.dart';
 | 
				
			||||||
import 'package:solian/providers/auth.dart';
 | 
					import 'package:solian/providers/auth.dart';
 | 
				
			||||||
 | 
					import 'package:solian/providers/chat.dart';
 | 
				
			||||||
import 'package:solian/router.dart';
 | 
					import 'package:solian/router.dart';
 | 
				
			||||||
import 'package:solian/utils/service_url.dart';
 | 
					import 'package:solian/utils/service_url.dart';
 | 
				
			||||||
import 'package:solian/widgets/chat/channel_action.dart';
 | 
					 | 
				
			||||||
import 'package:solian/widgets/chat/maintainer.dart';
 | 
					import 'package:solian/widgets/chat/maintainer.dart';
 | 
				
			||||||
import 'package:solian/widgets/chat/message.dart';
 | 
					import 'package:solian/widgets/chat/message.dart';
 | 
				
			||||||
import 'package:solian/widgets/chat/message_action.dart';
 | 
					import 'package:solian/widgets/chat/message_action.dart';
 | 
				
			||||||
import 'package:solian/widgets/chat/message_editor.dart';
 | 
					import 'package:solian/widgets/chat/message_editor.dart';
 | 
				
			||||||
import 'package:solian/widgets/exts.dart';
 | 
					 | 
				
			||||||
import 'package:solian/widgets/indent_wrapper.dart';
 | 
					import 'package:solian/widgets/indent_wrapper.dart';
 | 
				
			||||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
 | 
					import 'package:flutter_gen/gen_l10n/app_localizations.dart';
 | 
				
			||||||
import 'package:http/http.dart' as http;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
class ChatScreen extends StatefulWidget {
 | 
					class ChatScreen extends StatelessWidget {
 | 
				
			||||||
  final String alias;
 | 
					  final String alias;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const ChatScreen({super.key, required this.alias});
 | 
					  const ChatScreen({super.key, required this.alias});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  State<ChatScreen> createState() => _ChatScreenState();
 | 
					  Widget build(BuildContext context) {
 | 
				
			||||||
 | 
					    return IndentWrapper(
 | 
				
			||||||
 | 
					      title: AppLocalizations.of(context)!.post,
 | 
				
			||||||
 | 
					      noSafeArea: true,
 | 
				
			||||||
 | 
					      hideDrawer: true,
 | 
				
			||||||
 | 
					      child: ChatScreenWidget(
 | 
				
			||||||
 | 
					        alias: alias,
 | 
				
			||||||
 | 
					      ),
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class _ChatScreenState extends State<ChatScreen> {
 | 
					class ChatScreenWidget extends StatefulWidget {
 | 
				
			||||||
  Call? _ongoingCall;
 | 
					  final String alias;
 | 
				
			||||||
  Channel? _channelMeta;
 | 
					
 | 
				
			||||||
 | 
					  const ChatScreenWidget({super.key, required this.alias});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  State<ChatScreenWidget> createState() => _ChatScreenWidgetState();
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class _ChatScreenWidgetState extends State<ChatScreenWidget> {
 | 
				
			||||||
 | 
					  bool _isReady = false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  final PagingController<int, Message> _pagingController = PagingController(firstPageKey: 0);
 | 
					  final PagingController<int, Message> _pagingController = PagingController(firstPageKey: 0);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  final http.Client _client = http.Client();
 | 
					  late final ChatProvider _chat;
 | 
				
			||||||
 | 
					 | 
				
			||||||
  Future<Channel> fetchMetadata() async {
 | 
					 | 
				
			||||||
    var uri = getRequestUri('messaging', '/api/channels/${widget.alias}');
 | 
					 | 
				
			||||||
    var res = await _client.get(uri);
 | 
					 | 
				
			||||||
    if (res.statusCode == 200) {
 | 
					 | 
				
			||||||
      final result = jsonDecode(utf8.decode(res.bodyBytes));
 | 
					 | 
				
			||||||
      setState(() => _channelMeta = Channel.fromJson(result));
 | 
					 | 
				
			||||||
      return _channelMeta!;
 | 
					 | 
				
			||||||
    } else {
 | 
					 | 
				
			||||||
      var message = utf8.decode(res.bodyBytes);
 | 
					 | 
				
			||||||
      context.showErrorDialog(message);
 | 
					 | 
				
			||||||
      throw Exception(message);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  Future<Call?> fetchCall() async {
 | 
					 | 
				
			||||||
    var uri = getRequestUri('messaging', '/api/channels/${widget.alias}/calls/ongoing');
 | 
					 | 
				
			||||||
    var res = await _client.get(uri);
 | 
					 | 
				
			||||||
    if (res.statusCode == 200) {
 | 
					 | 
				
			||||||
      final result = jsonDecode(utf8.decode(res.bodyBytes));
 | 
					 | 
				
			||||||
      setState(() => _ongoingCall = Call.fromJson(result));
 | 
					 | 
				
			||||||
      return _ongoingCall;
 | 
					 | 
				
			||||||
    } else if (res.statusCode != 404) {
 | 
					 | 
				
			||||||
      var message = utf8.decode(res.bodyBytes);
 | 
					 | 
				
			||||||
      context.showErrorDialog(message);
 | 
					 | 
				
			||||||
      throw Exception(message);
 | 
					 | 
				
			||||||
    } else {
 | 
					 | 
				
			||||||
      return null;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  Future<void> fetchMessages(int pageKey, BuildContext context) async {
 | 
					  Future<void> fetchMessages(int pageKey, BuildContext context) async {
 | 
				
			||||||
    final auth = context.read<AuthProvider>();
 | 
					    final auth = context.read<AuthProvider>();
 | 
				
			||||||
@@ -142,11 +125,6 @@ class _ChatScreenState extends State<ChatScreen> {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  void initState() {
 | 
					  void initState() {
 | 
				
			||||||
    Future.delayed(Duration.zero, () {
 | 
					 | 
				
			||||||
      fetchMetadata();
 | 
					 | 
				
			||||||
      fetchCall();
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    _pagingController.addPageRequestListener((pageKey) => fetchMessages(pageKey, context));
 | 
					    _pagingController.addPageRequestListener((pageKey) => fetchMessages(pageKey, context));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    super.initState();
 | 
					    super.initState();
 | 
				
			||||||
@@ -180,6 +158,11 @@ class _ChatScreenState extends State<ChatScreen> {
 | 
				
			|||||||
      );
 | 
					      );
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (!_isReady) {
 | 
				
			||||||
 | 
					      _isReady = true;
 | 
				
			||||||
 | 
					      _chat = context.watch<ChatProvider>();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    final callBanner = MaterialBanner(
 | 
					    final callBanner = MaterialBanner(
 | 
				
			||||||
      padding: const EdgeInsets.only(top: 4, bottom: 4, left: 20),
 | 
					      padding: const EdgeInsets.only(top: 4, bottom: 4, left: 20),
 | 
				
			||||||
      leading: const Icon(Icons.call_received),
 | 
					      leading: const Icon(Icons.call_received),
 | 
				
			||||||
@@ -192,7 +175,7 @@ class _ChatScreenState extends State<ChatScreen> {
 | 
				
			|||||||
          onPressed: () {
 | 
					          onPressed: () {
 | 
				
			||||||
            router.pushNamed(
 | 
					            router.pushNamed(
 | 
				
			||||||
              'chat.channel.call',
 | 
					              'chat.channel.call',
 | 
				
			||||||
              extra: _ongoingCall,
 | 
					              extra: _chat.ongoingCall,
 | 
				
			||||||
              pathParameters: {'channel': widget.alias},
 | 
					              pathParameters: {'channel': widget.alias},
 | 
				
			||||||
            );
 | 
					            );
 | 
				
			||||||
          },
 | 
					          },
 | 
				
			||||||
@@ -200,69 +183,52 @@ class _ChatScreenState extends State<ChatScreen> {
 | 
				
			|||||||
      ],
 | 
					      ],
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return IndentWrapper(
 | 
					    return FutureBuilder(
 | 
				
			||||||
      hideDrawer: true,
 | 
					      future: _chat.fetchChannel(widget.alias),
 | 
				
			||||||
      title: _channelMeta?.name ?? 'Loading...',
 | 
					      builder: (context, snapshot) {
 | 
				
			||||||
      appBarActions: _channelMeta != null
 | 
					        if (!snapshot.hasData || snapshot.data == null) {
 | 
				
			||||||
          ? [
 | 
					          return const Center(child: CircularProgressIndicator());
 | 
				
			||||||
              ChannelCallAction(
 | 
					        }
 | 
				
			||||||
                call: _ongoingCall,
 | 
					 | 
				
			||||||
                channel: _channelMeta!,
 | 
					 | 
				
			||||||
                onUpdate: () => fetchMetadata(),
 | 
					 | 
				
			||||||
              ),
 | 
					 | 
				
			||||||
              ChannelManageAction(
 | 
					 | 
				
			||||||
                channel: _channelMeta!,
 | 
					 | 
				
			||||||
                onUpdate: () => fetchMetadata(),
 | 
					 | 
				
			||||||
              ),
 | 
					 | 
				
			||||||
            ]
 | 
					 | 
				
			||||||
          : [],
 | 
					 | 
				
			||||||
      child: FutureBuilder(
 | 
					 | 
				
			||||||
        future: fetchMetadata(),
 | 
					 | 
				
			||||||
        builder: (context, snapshot) {
 | 
					 | 
				
			||||||
          if (!snapshot.hasData || snapshot.data == null) {
 | 
					 | 
				
			||||||
            return const Center(child: CircularProgressIndicator());
 | 
					 | 
				
			||||||
          }
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
          return ChatMaintainer(
 | 
					        return ChatMaintainer(
 | 
				
			||||||
            channel: snapshot.data!,
 | 
					          channel: snapshot.data!,
 | 
				
			||||||
            child: Stack(
 | 
					          child: Stack(
 | 
				
			||||||
              children: [
 | 
					            children: [
 | 
				
			||||||
                Column(
 | 
					              Column(
 | 
				
			||||||
                  children: [
 | 
					                children: [
 | 
				
			||||||
                    Expanded(
 | 
					                  Expanded(
 | 
				
			||||||
                      child: PagedListView<int, Message>(
 | 
					                    child: PagedListView<int, Message>(
 | 
				
			||||||
                        reverse: true,
 | 
					                      reverse: true,
 | 
				
			||||||
                        pagingController: _pagingController,
 | 
					                      pagingController: _pagingController,
 | 
				
			||||||
                        builderDelegate: PagedChildBuilderDelegate<Message>(
 | 
					                      builderDelegate: PagedChildBuilderDelegate<Message>(
 | 
				
			||||||
                          animateTransitions: true,
 | 
					                        animateTransitions: true,
 | 
				
			||||||
                          transitionDuration: 500.ms,
 | 
					                        transitionDuration: 500.ms,
 | 
				
			||||||
                          itemBuilder: chatHistoryBuilder,
 | 
					                        itemBuilder: chatHistoryBuilder,
 | 
				
			||||||
                          noItemsFoundIndicatorBuilder: (_) => Container(),
 | 
					                        noItemsFoundIndicatorBuilder: (_) => Container(),
 | 
				
			||||||
                        ),
 | 
					 | 
				
			||||||
                      ),
 | 
					                      ),
 | 
				
			||||||
                    ),
 | 
					                    ),
 | 
				
			||||||
                    ChatMessageEditor(
 | 
					                  ),
 | 
				
			||||||
                      channel: widget.alias,
 | 
					                  ChatMessageEditor(
 | 
				
			||||||
                      editing: _editingItem,
 | 
					                    channel: widget.alias,
 | 
				
			||||||
                      replying: _replyingItem,
 | 
					                    editing: _editingItem,
 | 
				
			||||||
                      onReset: () => setState(() {
 | 
					                    replying: _replyingItem,
 | 
				
			||||||
                        _editingItem = null;
 | 
					                    onReset: () => setState(() {
 | 
				
			||||||
                        _replyingItem = null;
 | 
					                      _editingItem = null;
 | 
				
			||||||
                      }),
 | 
					                      _replyingItem = null;
 | 
				
			||||||
                    ),
 | 
					                    }),
 | 
				
			||||||
                  ],
 | 
					                  ),
 | 
				
			||||||
                ),
 | 
					                ],
 | 
				
			||||||
                _ongoingCall != null ? callBanner.animate().slideY() : Container(),
 | 
					              ),
 | 
				
			||||||
              ],
 | 
					              _chat.ongoingCall != null ? callBanner.animate().slideY() : Container(),
 | 
				
			||||||
            ),
 | 
					            ],
 | 
				
			||||||
            onInsertMessage: (message) => addMessage(message),
 | 
					          ),
 | 
				
			||||||
            onUpdateMessage: (message) => updateMessage(message),
 | 
					          onInsertMessage: (message) => addMessage(message),
 | 
				
			||||||
            onDeleteMessage: (message) => deleteMessage(message),
 | 
					          onUpdateMessage: (message) => updateMessage(message),
 | 
				
			||||||
            onCallStarted: (call) => setState(() => _ongoingCall = call),
 | 
					          onDeleteMessage: (message) => deleteMessage(message),
 | 
				
			||||||
            onCallEnded: () => setState(() => _ongoingCall = null),
 | 
					          onCallStarted: (call) => _chat.setOngoingCall(call),
 | 
				
			||||||
          );
 | 
					          onCallEnded: () => _chat.setOngoingCall(null),
 | 
				
			||||||
        },
 | 
					        );
 | 
				
			||||||
      ),
 | 
					      },
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -5,8 +5,10 @@ import 'package:provider/provider.dart';
 | 
				
			|||||||
import 'package:solian/models/channel.dart';
 | 
					import 'package:solian/models/channel.dart';
 | 
				
			||||||
import 'package:solian/providers/auth.dart';
 | 
					import 'package:solian/providers/auth.dart';
 | 
				
			||||||
import 'package:solian/router.dart';
 | 
					import 'package:solian/router.dart';
 | 
				
			||||||
 | 
					import 'package:solian/screens/chat/chat.dart';
 | 
				
			||||||
import 'package:solian/utils/service_url.dart';
 | 
					import 'package:solian/utils/service_url.dart';
 | 
				
			||||||
import 'package:solian/widgets/chat/chat_new.dart';
 | 
					import 'package:solian/widgets/chat/chat_new.dart';
 | 
				
			||||||
 | 
					import 'package:solian/widgets/empty.dart';
 | 
				
			||||||
import 'package:solian/widgets/exts.dart';
 | 
					import 'package:solian/widgets/exts.dart';
 | 
				
			||||||
import 'package:solian/widgets/indent_wrapper.dart';
 | 
					import 'package:solian/widgets/indent_wrapper.dart';
 | 
				
			||||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
 | 
					import 'package:flutter_gen/gen_l10n/app_localizations.dart';
 | 
				
			||||||
@@ -21,6 +23,68 @@ class ChatIndexScreen extends StatefulWidget {
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class _ChatIndexScreenState extends State<ChatIndexScreen> {
 | 
					class _ChatIndexScreenState extends State<ChatIndexScreen> {
 | 
				
			||||||
 | 
					  Channel? _selectedChannel;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  Widget build(BuildContext context) {
 | 
				
			||||||
 | 
					    final screenWidth = MediaQuery.of(context).size.width;
 | 
				
			||||||
 | 
					    final isLargeScreen = screenWidth >= 600;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return IndentWrapper(
 | 
				
			||||||
 | 
					      title: AppLocalizations.of(context)!.chat,
 | 
				
			||||||
 | 
					      appBarActions: const [NotificationButton()],
 | 
				
			||||||
 | 
					      fixedAppBarColor: isLargeScreen,
 | 
				
			||||||
 | 
					      child: isLargeScreen
 | 
				
			||||||
 | 
					          ? Row(
 | 
				
			||||||
 | 
					              children: [
 | 
				
			||||||
 | 
					                Flexible(
 | 
				
			||||||
 | 
					                  flex: 2,
 | 
				
			||||||
 | 
					                  child: ChatIndexScreenWidget(
 | 
				
			||||||
 | 
					                    onSelect: (item) {
 | 
				
			||||||
 | 
					                      setState(() => _selectedChannel = item);
 | 
				
			||||||
 | 
					                    },
 | 
				
			||||||
 | 
					                  ),
 | 
				
			||||||
 | 
					                ),
 | 
				
			||||||
 | 
					                const VerticalDivider(thickness: 0.3, width: 0.3),
 | 
				
			||||||
 | 
					                Flexible(
 | 
				
			||||||
 | 
					                  flex: 4,
 | 
				
			||||||
 | 
					                  child: _selectedChannel == null
 | 
				
			||||||
 | 
					                      ? const SelectionEmptyWidget()
 | 
				
			||||||
 | 
					                      : ChatScreenWidget(
 | 
				
			||||||
 | 
					                          key: Key('c${_selectedChannel!.id}'),
 | 
				
			||||||
 | 
					                          alias: _selectedChannel!.alias,
 | 
				
			||||||
 | 
					                        ),
 | 
				
			||||||
 | 
					                ),
 | 
				
			||||||
 | 
					              ],
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					          : ChatIndexScreenWidget(
 | 
				
			||||||
 | 
					              onSelect: (item) async {
 | 
				
			||||||
 | 
					                final result = await router.pushNamed(
 | 
				
			||||||
 | 
					                  'chat.channel',
 | 
				
			||||||
 | 
					                  pathParameters: {
 | 
				
			||||||
 | 
					                    'channel': item.alias,
 | 
				
			||||||
 | 
					                  },
 | 
				
			||||||
 | 
					                );
 | 
				
			||||||
 | 
					                switch (result) {
 | 
				
			||||||
 | 
					                  case 'refresh':
 | 
				
			||||||
 | 
					                  // fetchChannels();
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					              },
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class ChatIndexScreenWidget extends StatefulWidget {
 | 
				
			||||||
 | 
					  final Function(Channel item) onSelect;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const ChatIndexScreenWidget({super.key, required this.onSelect});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  State<ChatIndexScreenWidget> createState() => _ChatIndexScreenWidgetState();
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class _ChatIndexScreenWidgetState extends State<ChatIndexScreenWidget> {
 | 
				
			||||||
  List<Channel> _channels = List.empty();
 | 
					  List<Channel> _channels = List.empty();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  Future<void> fetchChannels() async {
 | 
					  Future<void> fetchChannels() async {
 | 
				
			||||||
@@ -61,9 +125,7 @@ class _ChatIndexScreenState extends State<ChatIndexScreen> {
 | 
				
			|||||||
  Widget build(BuildContext context) {
 | 
					  Widget build(BuildContext context) {
 | 
				
			||||||
    final auth = context.read<AuthProvider>();
 | 
					    final auth = context.read<AuthProvider>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return IndentWrapper(
 | 
					    return Scaffold(
 | 
				
			||||||
      title: AppLocalizations.of(context)!.chat,
 | 
					 | 
				
			||||||
      appBarActions: const [NotificationButton()],
 | 
					 | 
				
			||||||
      floatingActionButton: FutureBuilder(
 | 
					      floatingActionButton: FutureBuilder(
 | 
				
			||||||
        future: auth.isAuthorized(),
 | 
					        future: auth.isAuthorized(),
 | 
				
			||||||
        builder: (context, snapshot) {
 | 
					        builder: (context, snapshot) {
 | 
				
			||||||
@@ -78,43 +140,33 @@ class _ChatIndexScreenState extends State<ChatIndexScreen> {
 | 
				
			|||||||
          }
 | 
					          }
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
      ),
 | 
					      ),
 | 
				
			||||||
      child: FutureBuilder(
 | 
					      body: FutureBuilder(
 | 
				
			||||||
          future: auth.isAuthorized(),
 | 
					        future: auth.isAuthorized(),
 | 
				
			||||||
          builder: (context, snapshot) {
 | 
					        builder: (context, snapshot) {
 | 
				
			||||||
            if (!snapshot.hasData || !snapshot.data!) {
 | 
					          if (!snapshot.hasData || !snapshot.data!) {
 | 
				
			||||||
              return const SignInRequiredScreen();
 | 
					            return const SignInRequiredScreen();
 | 
				
			||||||
            }
 | 
					          }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            return RefreshIndicator(
 | 
					          return RefreshIndicator(
 | 
				
			||||||
              onRefresh: () => fetchChannels(),
 | 
					            onRefresh: () => fetchChannels(),
 | 
				
			||||||
              child: ListView.builder(
 | 
					            child: ListView.builder(
 | 
				
			||||||
                itemCount: _channels.length,
 | 
					              itemCount: _channels.length,
 | 
				
			||||||
                itemBuilder: (context, index) {
 | 
					              itemBuilder: (context, index) {
 | 
				
			||||||
                  final element = _channels[index];
 | 
					                final element = _channels[index];
 | 
				
			||||||
                  return ListTile(
 | 
					                return ListTile(
 | 
				
			||||||
                    leading: const CircleAvatar(
 | 
					                  leading: const CircleAvatar(
 | 
				
			||||||
                      backgroundColor: Colors.indigo,
 | 
					                    backgroundColor: Colors.indigo,
 | 
				
			||||||
                      child: Icon(Icons.tag, color: Colors.white),
 | 
					                    child: Icon(Icons.tag, color: Colors.white),
 | 
				
			||||||
                    ),
 | 
					                  ),
 | 
				
			||||||
                    title: Text(element.name),
 | 
					                  title: Text(element.name),
 | 
				
			||||||
                    subtitle: Text(element.description),
 | 
					                  subtitle: Text(element.description),
 | 
				
			||||||
                    onTap: () async {
 | 
					                  onTap: () => widget.onSelect(element),
 | 
				
			||||||
                      final result = await router.pushNamed(
 | 
					                );
 | 
				
			||||||
                        'chat.channel',
 | 
					              },
 | 
				
			||||||
                        pathParameters: {
 | 
					            ),
 | 
				
			||||||
                          'channel': element.alias,
 | 
					          );
 | 
				
			||||||
                        },
 | 
					        },
 | 
				
			||||||
                      );
 | 
					      ),
 | 
				
			||||||
                      switch (result) {
 | 
					 | 
				
			||||||
                        case 'refresh':
 | 
					 | 
				
			||||||
                          fetchChannels();
 | 
					 | 
				
			||||||
                      }
 | 
					 | 
				
			||||||
                    },
 | 
					 | 
				
			||||||
                  );
 | 
					 | 
				
			||||||
                },
 | 
					 | 
				
			||||||
              ),
 | 
					 | 
				
			||||||
            );
 | 
					 | 
				
			||||||
          }),
 | 
					 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -6,10 +6,12 @@ import 'package:solian/models/pagination.dart';
 | 
				
			|||||||
import 'package:solian/models/post.dart';
 | 
					import 'package:solian/models/post.dart';
 | 
				
			||||||
import 'package:solian/providers/auth.dart';
 | 
					import 'package:solian/providers/auth.dart';
 | 
				
			||||||
import 'package:solian/router.dart';
 | 
					import 'package:solian/router.dart';
 | 
				
			||||||
 | 
					import 'package:solian/screens/posts/screen.dart';
 | 
				
			||||||
import 'package:solian/utils/service_url.dart';
 | 
					import 'package:solian/utils/service_url.dart';
 | 
				
			||||||
import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
 | 
					import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
 | 
				
			||||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
 | 
					import 'package:flutter_gen/gen_l10n/app_localizations.dart';
 | 
				
			||||||
import 'package:http/http.dart' as http;
 | 
					import 'package:http/http.dart' as http;
 | 
				
			||||||
 | 
					import 'package:solian/widgets/empty.dart';
 | 
				
			||||||
import 'package:solian/widgets/indent_wrapper.dart';
 | 
					import 'package:solian/widgets/indent_wrapper.dart';
 | 
				
			||||||
import 'package:solian/widgets/notification_notifier.dart';
 | 
					import 'package:solian/widgets/notification_notifier.dart';
 | 
				
			||||||
import 'package:solian/widgets/posts/item.dart';
 | 
					import 'package:solian/widgets/posts/item.dart';
 | 
				
			||||||
@@ -22,8 +24,68 @@ class ExploreScreen extends StatefulWidget {
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class _ExploreScreenState extends State<ExploreScreen> {
 | 
					class _ExploreScreenState extends State<ExploreScreen> {
 | 
				
			||||||
  final PagingController<int, Post> _pagingController =
 | 
					  Post? _selectedPost;
 | 
				
			||||||
      PagingController(firstPageKey: 0);
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  Widget build(BuildContext context) {
 | 
				
			||||||
 | 
					    final screenWidth = MediaQuery.of(context).size.width;
 | 
				
			||||||
 | 
					    final isLargeScreen = screenWidth >= 600;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return IndentWrapper(
 | 
				
			||||||
 | 
					      noSafeArea: true,
 | 
				
			||||||
 | 
					      fixedAppBarColor: isLargeScreen,
 | 
				
			||||||
 | 
					      appBarActions: const [NotificationButton()],
 | 
				
			||||||
 | 
					      title: AppLocalizations.of(context)!.explore,
 | 
				
			||||||
 | 
					      child: isLargeScreen
 | 
				
			||||||
 | 
					          ? Row(
 | 
				
			||||||
 | 
					              children: [
 | 
				
			||||||
 | 
					                Flexible(
 | 
				
			||||||
 | 
					                  flex: 2,
 | 
				
			||||||
 | 
					                  child: ExploreScreenWidget(
 | 
				
			||||||
 | 
					                    onSelect: (item) {
 | 
				
			||||||
 | 
					                      setState(() => _selectedPost = item);
 | 
				
			||||||
 | 
					                    },
 | 
				
			||||||
 | 
					                  ),
 | 
				
			||||||
 | 
					                ),
 | 
				
			||||||
 | 
					                const VerticalDivider(thickness: 0.3, width: 0.3),
 | 
				
			||||||
 | 
					                Flexible(
 | 
				
			||||||
 | 
					                  flex: 4,
 | 
				
			||||||
 | 
					                  child: _selectedPost == null
 | 
				
			||||||
 | 
					                      ? const SelectionEmptyWidget()
 | 
				
			||||||
 | 
					                      : PostScreenWidget(
 | 
				
			||||||
 | 
					                          key: Key('p${_selectedPost!.id}'),
 | 
				
			||||||
 | 
					                          dataset: _selectedPost!.dataset,
 | 
				
			||||||
 | 
					                          alias: _selectedPost!.alias,
 | 
				
			||||||
 | 
					                        ),
 | 
				
			||||||
 | 
					                ),
 | 
				
			||||||
 | 
					              ],
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					          : ExploreScreenWidget(
 | 
				
			||||||
 | 
					              onSelect: (item) {
 | 
				
			||||||
 | 
					                router.pushNamed(
 | 
				
			||||||
 | 
					                  'posts.screen',
 | 
				
			||||||
 | 
					                  pathParameters: {
 | 
				
			||||||
 | 
					                    'alias': item.alias,
 | 
				
			||||||
 | 
					                    'dataset': item.dataset,
 | 
				
			||||||
 | 
					                  },
 | 
				
			||||||
 | 
					                );
 | 
				
			||||||
 | 
					              },
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class ExploreScreenWidget extends StatefulWidget {
 | 
				
			||||||
 | 
					  final Function(Post item) onSelect;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const ExploreScreenWidget({super.key, required this.onSelect});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  State<ExploreScreenWidget> createState() => _ExploreScreenWidgetState();
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class _ExploreScreenWidgetState extends State<ExploreScreenWidget> {
 | 
				
			||||||
 | 
					  final PagingController<int, Post> _pagingController = PagingController(firstPageKey: 0);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  final http.Client _client = http.Client();
 | 
					  final http.Client _client = http.Client();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -31,15 +93,12 @@ class _ExploreScreenState extends State<ExploreScreen> {
 | 
				
			|||||||
    final offset = pageKey;
 | 
					    final offset = pageKey;
 | 
				
			||||||
    const take = 5;
 | 
					    const take = 5;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    var uri =
 | 
					    var uri = getRequestUri('interactive', '/api/feed?take=$take&offset=$offset');
 | 
				
			||||||
        getRequestUri('interactive', '/api/feed?take=$take&offset=$offset');
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    var res = await _client.get(uri);
 | 
					    var res = await _client.get(uri);
 | 
				
			||||||
    if (res.statusCode == 200) {
 | 
					    if (res.statusCode == 200) {
 | 
				
			||||||
      final result =
 | 
					      final result = PaginationResult.fromJson(jsonDecode(utf8.decode(res.bodyBytes)));
 | 
				
			||||||
          PaginationResult.fromJson(jsonDecode(utf8.decode(res.bodyBytes)));
 | 
					      final items = result.data?.map((x) => Post.fromJson(x)).toList() ?? List.empty();
 | 
				
			||||||
      final items =
 | 
					 | 
				
			||||||
          result.data?.map((x) => Post.fromJson(x)).toList() ?? List.empty();
 | 
					 | 
				
			||||||
      final isLastPage = (result.count - pageKey) < take;
 | 
					      final isLastPage = (result.count - pageKey) < take;
 | 
				
			||||||
      if (isLastPage || result.data == null) {
 | 
					      if (isLastPage || result.data == null) {
 | 
				
			||||||
        _pagingController.appendLastPage(items);
 | 
					        _pagingController.appendLastPage(items);
 | 
				
			||||||
@@ -63,8 +122,7 @@ class _ExploreScreenState extends State<ExploreScreen> {
 | 
				
			|||||||
  Widget build(BuildContext context) {
 | 
					  Widget build(BuildContext context) {
 | 
				
			||||||
    final auth = context.read<AuthProvider>();
 | 
					    final auth = context.read<AuthProvider>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return IndentWrapper(
 | 
					    return Scaffold(
 | 
				
			||||||
      noSafeArea: true,
 | 
					 | 
				
			||||||
      floatingActionButton: FutureBuilder(
 | 
					      floatingActionButton: FutureBuilder(
 | 
				
			||||||
        future: auth.isAuthorized(),
 | 
					        future: auth.isAuthorized(),
 | 
				
			||||||
        builder: (context, snapshot) {
 | 
					        builder: (context, snapshot) {
 | 
				
			||||||
@@ -72,7 +130,7 @@ class _ExploreScreenState extends State<ExploreScreen> {
 | 
				
			|||||||
            return FloatingActionButton(
 | 
					            return FloatingActionButton(
 | 
				
			||||||
              child: const Icon(Icons.edit),
 | 
					              child: const Icon(Icons.edit),
 | 
				
			||||||
              onPressed: () async {
 | 
					              onPressed: () async {
 | 
				
			||||||
                final did = await router.pushNamed("posts.moments.editor");
 | 
					                final did = await router.pushNamed('posts.moments.editor');
 | 
				
			||||||
                if (did == true) _pagingController.refresh();
 | 
					                if (did == true) _pagingController.refresh();
 | 
				
			||||||
              },
 | 
					              },
 | 
				
			||||||
            );
 | 
					            );
 | 
				
			||||||
@@ -81,9 +139,7 @@ class _ExploreScreenState extends State<ExploreScreen> {
 | 
				
			|||||||
          }
 | 
					          }
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
      ),
 | 
					      ),
 | 
				
			||||||
      appBarActions: const [NotificationButton()],
 | 
					      body: RefreshIndicator(
 | 
				
			||||||
      title: AppLocalizations.of(context)!.explore,
 | 
					 | 
				
			||||||
      child: RefreshIndicator(
 | 
					 | 
				
			||||||
        onRefresh: () => Future.sync(
 | 
					        onRefresh: () => Future.sync(
 | 
				
			||||||
          () => _pagingController.refresh(),
 | 
					          () => _pagingController.refresh(),
 | 
				
			||||||
        ),
 | 
					        ),
 | 
				
			||||||
@@ -93,15 +149,7 @@ class _ExploreScreenState extends State<ExploreScreen> {
 | 
				
			|||||||
            itemBuilder: (context, item, index) => PostItem(
 | 
					            itemBuilder: (context, item, index) => PostItem(
 | 
				
			||||||
              item: item,
 | 
					              item: item,
 | 
				
			||||||
              onUpdate: () => _pagingController.refresh(),
 | 
					              onUpdate: () => _pagingController.refresh(),
 | 
				
			||||||
              onTap: () {
 | 
					              onTap: () => widget.onSelect(item),
 | 
				
			||||||
                router.pushNamed(
 | 
					 | 
				
			||||||
                  'posts.screen',
 | 
					 | 
				
			||||||
                  pathParameters: {
 | 
					 | 
				
			||||||
                    'alias': item.alias,
 | 
					 | 
				
			||||||
                    'dataset': '${item.modelType}s',
 | 
					 | 
				
			||||||
                  },
 | 
					 | 
				
			||||||
                );
 | 
					 | 
				
			||||||
              },
 | 
					 | 
				
			||||||
            ),
 | 
					            ),
 | 
				
			||||||
          ),
 | 
					          ),
 | 
				
			||||||
        ),
 | 
					        ),
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -97,7 +97,7 @@ class NotificationItem extends StatelessWidget {
 | 
				
			|||||||
              padding: const EdgeInsets.only(
 | 
					              padding: const EdgeInsets.only(
 | 
				
			||||||
                  left: 16, right: 16, top: 34, bottom: 12),
 | 
					                  left: 16, right: 16, top: 34, bottom: 12),
 | 
				
			||||||
              child: Text(
 | 
					              child: Text(
 | 
				
			||||||
                "Links",
 | 
					                'Links',
 | 
				
			||||||
                style: Theme.of(context).textTheme.headlineSmall,
 | 
					                style: Theme.of(context).textTheme.headlineSmall,
 | 
				
			||||||
              ),
 | 
					              ),
 | 
				
			||||||
            ),
 | 
					            ),
 | 
				
			||||||
@@ -151,7 +151,7 @@ class NotificationItem extends StatelessWidget {
 | 
				
			|||||||
                      text: item.subject,
 | 
					                      text: item.subject,
 | 
				
			||||||
                      style: const TextStyle(fontWeight: FontWeight.bold),
 | 
					                      style: const TextStyle(fontWeight: FontWeight.bold),
 | 
				
			||||||
                    ),
 | 
					                    ),
 | 
				
			||||||
                    const TextSpan(text: " is marked as read")
 | 
					                    const TextSpan(text: ' is marked as read')
 | 
				
			||||||
                  ],
 | 
					                  ],
 | 
				
			||||||
                ),
 | 
					                ),
 | 
				
			||||||
              ),
 | 
					              ),
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -65,7 +65,7 @@ class _CommentEditorScreenState extends State<CommentEditorScreen> {
 | 
				
			|||||||
        ? getRequestUri('interactive', '/api/p/$relatedDataset/$alias/comments')
 | 
					        ? getRequestUri('interactive', '/api/p/$relatedDataset/$alias/comments')
 | 
				
			||||||
        : getRequestUri('interactive', '/api/p/comments/${widget.editing!.id}');
 | 
					        : getRequestUri('interactive', '/api/p/comments/${widget.editing!.id}');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    final req = Request(widget.editing == null ? "POST" : "PUT", uri);
 | 
					    final req = Request(widget.editing == null ? 'POST' : 'PUT', uri);
 | 
				
			||||||
    req.headers['Content-Type'] = 'application/json';
 | 
					    req.headers['Content-Type'] = 'application/json';
 | 
				
			||||||
    req.body = jsonEncode(<String, dynamic>{
 | 
					    req.body = jsonEncode(<String, dynamic>{
 | 
				
			||||||
      'alias': _alias,
 | 
					      'alias': _alias,
 | 
				
			||||||
@@ -140,12 +140,12 @@ class _CommentEditorScreenState extends State<CommentEditorScreen> {
 | 
				
			|||||||
              if (snapshot.hasData) {
 | 
					              if (snapshot.hasData) {
 | 
				
			||||||
                var userinfo = snapshot.data;
 | 
					                var userinfo = snapshot.data;
 | 
				
			||||||
                return ListTile(
 | 
					                return ListTile(
 | 
				
			||||||
                  title: Text(userinfo["nick"]),
 | 
					                  title: Text(userinfo['nick']),
 | 
				
			||||||
                  subtitle: Text(
 | 
					                  subtitle: Text(
 | 
				
			||||||
                    AppLocalizations.of(context)!.postIdentityNotify,
 | 
					                    AppLocalizations.of(context)!.postIdentityNotify,
 | 
				
			||||||
                  ),
 | 
					                  ),
 | 
				
			||||||
                  leading: AccountAvatar(
 | 
					                  leading: AccountAvatar(
 | 
				
			||||||
                    source: userinfo["picture"],
 | 
					                    source: userinfo['picture'],
 | 
				
			||||||
                    direct: true,
 | 
					                    direct: true,
 | 
				
			||||||
                  ),
 | 
					                  ),
 | 
				
			||||||
                );
 | 
					                );
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -55,7 +55,7 @@ class _MomentEditorScreenState extends State<MomentEditorScreen> {
 | 
				
			|||||||
        ? getRequestUri('interactive', '/api/p/moments')
 | 
					        ? getRequestUri('interactive', '/api/p/moments')
 | 
				
			||||||
        : getRequestUri('interactive', '/api/p/moments/${widget.editing!.id}');
 | 
					        : getRequestUri('interactive', '/api/p/moments/${widget.editing!.id}');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    final req = Request(widget.editing == null ? "POST" : "PUT", uri);
 | 
					    final req = Request(widget.editing == null ? 'POST' : 'PUT', uri);
 | 
				
			||||||
    req.headers['Content-Type'] = 'application/json';
 | 
					    req.headers['Content-Type'] = 'application/json';
 | 
				
			||||||
    req.body = jsonEncode(<String, dynamic>{
 | 
					    req.body = jsonEncode(<String, dynamic>{
 | 
				
			||||||
      'alias': _alias,
 | 
					      'alias': _alias,
 | 
				
			||||||
@@ -130,12 +130,12 @@ class _MomentEditorScreenState extends State<MomentEditorScreen> {
 | 
				
			|||||||
              if (snapshot.hasData) {
 | 
					              if (snapshot.hasData) {
 | 
				
			||||||
                var userinfo = snapshot.data;
 | 
					                var userinfo = snapshot.data;
 | 
				
			||||||
                return ListTile(
 | 
					                return ListTile(
 | 
				
			||||||
                  title: Text(userinfo["nick"]),
 | 
					                  title: Text(userinfo['nick']),
 | 
				
			||||||
                  subtitle: Text(
 | 
					                  subtitle: Text(
 | 
				
			||||||
                    AppLocalizations.of(context)!.postIdentityNotify,
 | 
					                    AppLocalizations.of(context)!.postIdentityNotify,
 | 
				
			||||||
                  ),
 | 
					                  ),
 | 
				
			||||||
                  leading: AccountAvatar(
 | 
					                  leading: AccountAvatar(
 | 
				
			||||||
                    source: userinfo["picture"],
 | 
					                    source: userinfo['picture'],
 | 
				
			||||||
                    direct: true,
 | 
					                    direct: true,
 | 
				
			||||||
                  ),
 | 
					                  ),
 | 
				
			||||||
                );
 | 
					                );
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -11,25 +11,43 @@ import 'package:solian/widgets/posts/comment_list.dart';
 | 
				
			|||||||
import 'package:solian/widgets/posts/item.dart';
 | 
					import 'package:solian/widgets/posts/item.dart';
 | 
				
			||||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
 | 
					import 'package:flutter_gen/gen_l10n/app_localizations.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class PostScreen extends StatefulWidget {
 | 
					class PostScreen extends StatelessWidget {
 | 
				
			||||||
  final String dataset;
 | 
					  final String dataset;
 | 
				
			||||||
  final String alias;
 | 
					  final String alias;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const PostScreen({super.key, required this.alias, required this.dataset});
 | 
					  const PostScreen({super.key, required this.alias, required this.dataset});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  State<PostScreen> createState() => _PostScreenState();
 | 
					  Widget build(BuildContext context) {
 | 
				
			||||||
 | 
					    return IndentWrapper(
 | 
				
			||||||
 | 
					      title: AppLocalizations.of(context)!.post,
 | 
				
			||||||
 | 
					      noSafeArea: true,
 | 
				
			||||||
 | 
					      hideDrawer: true,
 | 
				
			||||||
 | 
					      child: PostScreenWidget(
 | 
				
			||||||
 | 
					        dataset: dataset,
 | 
				
			||||||
 | 
					        alias: alias,
 | 
				
			||||||
 | 
					      ),
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class _PostScreenState extends State<PostScreen> {
 | 
					class PostScreenWidget extends StatefulWidget {
 | 
				
			||||||
 | 
					  final String dataset;
 | 
				
			||||||
 | 
					  final String alias;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const PostScreenWidget({super.key, required this.dataset, required this.alias});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  State<PostScreenWidget> createState() => _PostScreenWidgetState();
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class _PostScreenWidgetState extends State<PostScreenWidget> {
 | 
				
			||||||
  final _client = http.Client();
 | 
					  final _client = http.Client();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  final PagingController<int, Post> _commentPagingController =
 | 
					  final PagingController<int, Post> _commentPagingController = PagingController(firstPageKey: 0);
 | 
				
			||||||
      PagingController(firstPageKey: 0);
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  Future<Post?> fetchPost(BuildContext context) async {
 | 
					  Future<Post?> fetchPost(BuildContext context) async {
 | 
				
			||||||
    final uri = getRequestUri(
 | 
					    final uri = getRequestUri('interactive', '/api/p/${widget.dataset}/${widget.alias}');
 | 
				
			||||||
        'interactive', '/api/p/${widget.dataset}/${widget.alias}');
 | 
					 | 
				
			||||||
    final res = await _client.get(uri);
 | 
					    final res = await _client.get(uri);
 | 
				
			||||||
    if (res.statusCode != 200) {
 | 
					    if (res.statusCode != 200) {
 | 
				
			||||||
      final err = utf8.decode(res.bodyBytes);
 | 
					      final err = utf8.decode(res.bodyBytes);
 | 
				
			||||||
@@ -42,43 +60,38 @@ class _PostScreenState extends State<PostScreen> {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  Widget build(BuildContext context) {
 | 
					  Widget build(BuildContext context) {
 | 
				
			||||||
    return IndentWrapper(
 | 
					    return FutureBuilder(
 | 
				
			||||||
      noSafeArea: true,
 | 
					      future: fetchPost(context),
 | 
				
			||||||
      hideDrawer: true,
 | 
					      builder: (context, snapshot) {
 | 
				
			||||||
      title: AppLocalizations.of(context)!.post,
 | 
					        if (snapshot.hasData && snapshot.data != null) {
 | 
				
			||||||
      child: FutureBuilder(
 | 
					          return CustomScrollView(
 | 
				
			||||||
        future: fetchPost(context),
 | 
					            slivers: [
 | 
				
			||||||
        builder: (context, snapshot) {
 | 
					              SliverToBoxAdapter(
 | 
				
			||||||
          if (snapshot.hasData && snapshot.data != null) {
 | 
					                child: PostItem(
 | 
				
			||||||
            return CustomScrollView(
 | 
					                  item: snapshot.data!,
 | 
				
			||||||
              slivers: [
 | 
					                  brief: false,
 | 
				
			||||||
                SliverToBoxAdapter(
 | 
					                  ripple: false,
 | 
				
			||||||
                  child: PostItem(
 | 
					 | 
				
			||||||
                    item: snapshot.data!,
 | 
					 | 
				
			||||||
                    brief: false,
 | 
					 | 
				
			||||||
                    ripple: false,
 | 
					 | 
				
			||||||
                  ),
 | 
					 | 
				
			||||||
                ),
 | 
					                ),
 | 
				
			||||||
                SliverToBoxAdapter(
 | 
					              ),
 | 
				
			||||||
                  child: CommentListHeader(
 | 
					              SliverToBoxAdapter(
 | 
				
			||||||
                    related: snapshot.data!,
 | 
					                child: CommentListHeader(
 | 
				
			||||||
                    paging: _commentPagingController,
 | 
					 | 
				
			||||||
                  ),
 | 
					 | 
				
			||||||
                ),
 | 
					 | 
				
			||||||
                CommentList(
 | 
					 | 
				
			||||||
                  related: snapshot.data!,
 | 
					                  related: snapshot.data!,
 | 
				
			||||||
                  dataset: widget.dataset,
 | 
					 | 
				
			||||||
                  paging: _commentPagingController,
 | 
					                  paging: _commentPagingController,
 | 
				
			||||||
                ),
 | 
					                ),
 | 
				
			||||||
              ],
 | 
					              ),
 | 
				
			||||||
            );
 | 
					              CommentList(
 | 
				
			||||||
          } else {
 | 
					                related: snapshot.data!,
 | 
				
			||||||
            return const Center(
 | 
					                dataset: widget.dataset,
 | 
				
			||||||
              child: CircularProgressIndicator(),
 | 
					                paging: _commentPagingController,
 | 
				
			||||||
            );
 | 
					              ),
 | 
				
			||||||
          }
 | 
					            ],
 | 
				
			||||||
        },
 | 
					          );
 | 
				
			||||||
      ),
 | 
					        } else {
 | 
				
			||||||
 | 
					          return const Center(
 | 
				
			||||||
 | 
					            child: CircularProgressIndicator(),
 | 
				
			||||||
 | 
					          );
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -14,7 +14,7 @@ class CallOverlay extends StatelessWidget {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    final chat = context.watch<ChatProvider>();
 | 
					    final chat = context.watch<ChatProvider>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (chat.isShown || chat.call == null) {
 | 
					    if (chat.isCallShown || chat.currentCall == null) {
 | 
				
			||||||
      return Container();
 | 
					      return Container();
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -54,8 +54,8 @@ class CallOverlay extends StatelessWidget {
 | 
				
			|||||||
      onTap: () {
 | 
					      onTap: () {
 | 
				
			||||||
        router.pushNamed(
 | 
					        router.pushNamed(
 | 
				
			||||||
          'chat.channel.call',
 | 
					          'chat.channel.call',
 | 
				
			||||||
          extra: chat.call!.info,
 | 
					          extra: chat.currentCall!.info,
 | 
				
			||||||
          pathParameters: {'channel': chat.call!.channel.alias},
 | 
					          pathParameters: {'channel': chat.currentCall!.channel.alias},
 | 
				
			||||||
        );
 | 
					        );
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -73,9 +73,9 @@ class _ControlsWidgetState extends State<ControlsWidget> {
 | 
				
			|||||||
    if (await context.showDisconnectDialog() != true) return;
 | 
					    if (await context.showDisconnectDialog() != true) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    final chat = context.read<ChatProvider>();
 | 
					    final chat = context.read<ChatProvider>();
 | 
				
			||||||
    if (chat.call != null) {
 | 
					    if (chat.currentCall != null) {
 | 
				
			||||||
      chat.call!.deactivate();
 | 
					      chat.currentCall!.deactivate();
 | 
				
			||||||
      chat.call!.dispose();
 | 
					      chat.currentCall!.dispose();
 | 
				
			||||||
      router.pop();
 | 
					      router.pop();
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -55,23 +55,19 @@ class _ChatMaintainerState extends State<ChatMaintainer> {
 | 
				
			|||||||
          switch (result.method) {
 | 
					          switch (result.method) {
 | 
				
			||||||
            case 'messages.new':
 | 
					            case 'messages.new':
 | 
				
			||||||
              final payload = Message.fromJson(result.payload!);
 | 
					              final payload = Message.fromJson(result.payload!);
 | 
				
			||||||
              if (payload.channelId == widget.channel.id)
 | 
					              if (payload.channelId == widget.channel.id) widget.onInsertMessage(payload);
 | 
				
			||||||
                widget.onInsertMessage(payload);
 | 
					 | 
				
			||||||
              break;
 | 
					              break;
 | 
				
			||||||
            case 'messages.update':
 | 
					            case 'messages.update':
 | 
				
			||||||
              final payload = Message.fromJson(result.payload!);
 | 
					              final payload = Message.fromJson(result.payload!);
 | 
				
			||||||
              if (payload.channelId == widget.channel.id)
 | 
					              if (payload.channelId == widget.channel.id) widget.onUpdateMessage(payload);
 | 
				
			||||||
                widget.onUpdateMessage(payload);
 | 
					 | 
				
			||||||
              break;
 | 
					              break;
 | 
				
			||||||
            case 'messages.burnt':
 | 
					            case 'messages.burnt':
 | 
				
			||||||
              final payload = Message.fromJson(result.payload!);
 | 
					              final payload = Message.fromJson(result.payload!);
 | 
				
			||||||
              if (payload.channelId == widget.channel.id)
 | 
					              if (payload.channelId == widget.channel.id) widget.onDeleteMessage(payload);
 | 
				
			||||||
                widget.onDeleteMessage(payload);
 | 
					 | 
				
			||||||
              break;
 | 
					              break;
 | 
				
			||||||
            case 'calls.new':
 | 
					            case 'calls.new':
 | 
				
			||||||
              final payload = Call.fromJson(result.payload!);
 | 
					              final payload = Call.fromJson(result.payload!);
 | 
				
			||||||
              if (payload.channelId == widget.channel.id)
 | 
					              if (payload.channelId == widget.channel.id) widget.onCallStarted(payload);
 | 
				
			||||||
                widget.onCallStarted(payload);
 | 
					 | 
				
			||||||
              break;
 | 
					              break;
 | 
				
			||||||
            case 'calls.end':
 | 
					            case 'calls.end':
 | 
				
			||||||
              final payload = Call.fromJson(result.payload!);
 | 
					              final payload = Call.fromJson(result.payload!);
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -56,7 +56,7 @@ class _ChatMessageEditorState extends State<ChatMessageEditor> {
 | 
				
			|||||||
        ? getRequestUri('messaging', '/api/channels/${widget.channel}/messages')
 | 
					        ? getRequestUri('messaging', '/api/channels/${widget.channel}/messages')
 | 
				
			||||||
        : getRequestUri('messaging', '/api/channels/${widget.channel}/messages/${widget.editing!.id}');
 | 
					        : getRequestUri('messaging', '/api/channels/${widget.channel}/messages/${widget.editing!.id}');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    final req = Request(widget.editing == null ? "POST" : "PUT", uri);
 | 
					    final req = Request(widget.editing == null ? 'POST' : 'PUT', uri);
 | 
				
			||||||
    req.headers['Content-Type'] = 'application/json';
 | 
					    req.headers['Content-Type'] = 'application/json';
 | 
				
			||||||
    req.body = jsonEncode(<String, dynamic>{
 | 
					    req.body = jsonEncode(<String, dynamic>{
 | 
				
			||||||
      'content': _textController.value.text,
 | 
					      'content': _textController.value.text,
 | 
				
			||||||
@@ -163,7 +163,6 @@ class _ChatMessageEditorState extends State<ChatMessageEditor> {
 | 
				
			|||||||
                  focusNode: _focusNode,
 | 
					                  focusNode: _focusNode,
 | 
				
			||||||
                  controller: _textController,
 | 
					                  controller: _textController,
 | 
				
			||||||
                  maxLines: null,
 | 
					                  maxLines: null,
 | 
				
			||||||
                  autofocus: true,
 | 
					 | 
				
			||||||
                  autocorrect: true,
 | 
					                  autocorrect: true,
 | 
				
			||||||
                  keyboardType: TextInputType.text,
 | 
					                  keyboardType: TextInputType.text,
 | 
				
			||||||
                  decoration: InputDecoration.collapsed(
 | 
					                  decoration: InputDecoration.collapsed(
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -22,7 +22,11 @@ class LayoutWrapper extends StatelessWidget {
 | 
				
			|||||||
    final content = child ?? Container();
 | 
					    final content = child ?? Container();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return Scaffold(
 | 
					    return Scaffold(
 | 
				
			||||||
      appBar: AppBar(title: Text(title), actions: appBarActions),
 | 
					      appBar: AppBar(
 | 
				
			||||||
 | 
					        title: Text(title),
 | 
				
			||||||
 | 
					        actions: appBarActions,
 | 
				
			||||||
 | 
					        centerTitle: false,
 | 
				
			||||||
 | 
					      ),
 | 
				
			||||||
      floatingActionButton: floatingActionButton,
 | 
					      floatingActionButton: floatingActionButton,
 | 
				
			||||||
      drawer: const SolianNavigationDrawer(),
 | 
					      drawer: const SolianNavigationDrawer(),
 | 
				
			||||||
      body: noSafeArea ? content : SafeArea(child: content),
 | 
					      body: noSafeArea ? content : SafeArea(child: content),
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										23
									
								
								lib/widgets/empty.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								lib/widgets/empty.dart
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,23 @@
 | 
				
			|||||||
 | 
					import 'package:flutter/material.dart';
 | 
				
			||||||
 | 
					import 'package:flutter_gen/gen_l10n/app_localizations.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class SelectionEmptyWidget extends StatelessWidget {
 | 
				
			||||||
 | 
					  const SelectionEmptyWidget({super.key});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  Widget build(BuildContext context) {
 | 
				
			||||||
 | 
					    return Center(
 | 
				
			||||||
 | 
					      child: Column(
 | 
				
			||||||
 | 
					        mainAxisSize: MainAxisSize.min,
 | 
				
			||||||
 | 
					        children: [
 | 
				
			||||||
 | 
					          Image.asset('assets/logo.png', width: 64, height: 64),
 | 
				
			||||||
 | 
					          const SizedBox(height: 2),
 | 
				
			||||||
 | 
					          Text(
 | 
				
			||||||
 | 
					            AppLocalizations.of(context)!.appName,
 | 
				
			||||||
 | 
					            style: const TextStyle(fontSize: 18, fontWeight: FontWeight.w900),
 | 
				
			||||||
 | 
					          ),
 | 
				
			||||||
 | 
					        ],
 | 
				
			||||||
 | 
					      ),
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -9,8 +9,8 @@ extension SolianCommonExtensions on BuildContext {
 | 
				
			|||||||
      return message
 | 
					      return message
 | 
				
			||||||
          .split(' ')
 | 
					          .split(' ')
 | 
				
			||||||
          .map((element) =>
 | 
					          .map((element) =>
 | 
				
			||||||
              "${element[0].toUpperCase()}${element.substring(1).toLowerCase()}")
 | 
					              '${element[0].toUpperCase()}${element.substring(1).toLowerCase()}')
 | 
				
			||||||
          .join(" ");
 | 
					          .join(' ');
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return showDialog<void>(
 | 
					    return showDialog<void>(
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -5,6 +5,7 @@ import 'package:solian/widgets/navigation_drawer.dart';
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
class IndentWrapper extends LayoutWrapper {
 | 
					class IndentWrapper extends LayoutWrapper {
 | 
				
			||||||
  final bool hideDrawer;
 | 
					  final bool hideDrawer;
 | 
				
			||||||
 | 
					  final bool fixedAppBarColor;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const IndentWrapper({
 | 
					  const IndentWrapper({
 | 
				
			||||||
    super.key,
 | 
					    super.key,
 | 
				
			||||||
@@ -13,6 +14,7 @@ class IndentWrapper extends LayoutWrapper {
 | 
				
			|||||||
    super.floatingActionButton,
 | 
					    super.floatingActionButton,
 | 
				
			||||||
    super.appBarActions,
 | 
					    super.appBarActions,
 | 
				
			||||||
    this.hideDrawer = false,
 | 
					    this.hideDrawer = false,
 | 
				
			||||||
 | 
					    this.fixedAppBarColor = false,
 | 
				
			||||||
    super.noSafeArea = false,
 | 
					    super.noSafeArea = false,
 | 
				
			||||||
  }) : super();
 | 
					  }) : super();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -30,6 +32,8 @@ class IndentWrapper extends LayoutWrapper {
 | 
				
			|||||||
            : null,
 | 
					            : null,
 | 
				
			||||||
        title: Text(title),
 | 
					        title: Text(title),
 | 
				
			||||||
        actions: appBarActions,
 | 
					        actions: appBarActions,
 | 
				
			||||||
 | 
					        centerTitle: false,
 | 
				
			||||||
 | 
					        elevation: fixedAppBarColor ? 4 : null,
 | 
				
			||||||
      ),
 | 
					      ),
 | 
				
			||||||
      floatingActionButton: floatingActionButton,
 | 
					      floatingActionButton: floatingActionButton,
 | 
				
			||||||
      drawer: const SolianNavigationDrawer(),
 | 
					      drawer: const SolianNavigationDrawer(),
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -39,21 +39,21 @@ class _SolianNavigationDrawerState extends State<SolianNavigationDrawer> {
 | 
				
			|||||||
          icon: const Icon(Icons.explore),
 | 
					          icon: const Icon(Icons.explore),
 | 
				
			||||||
          label: Text(AppLocalizations.of(context)!.explore),
 | 
					          label: Text(AppLocalizations.of(context)!.explore),
 | 
				
			||||||
        ),
 | 
					        ),
 | 
				
			||||||
        "explore",
 | 
					        'explore',
 | 
				
			||||||
      ),
 | 
					      ),
 | 
				
			||||||
      (
 | 
					      (
 | 
				
			||||||
        NavigationDrawerDestination(
 | 
					        NavigationDrawerDestination(
 | 
				
			||||||
          icon: const Icon(Icons.send),
 | 
					          icon: const Icon(Icons.send),
 | 
				
			||||||
          label: Text(AppLocalizations.of(context)!.chat),
 | 
					          label: Text(AppLocalizations.of(context)!.chat),
 | 
				
			||||||
        ),
 | 
					        ),
 | 
				
			||||||
        "chat",
 | 
					        'chat',
 | 
				
			||||||
      ),
 | 
					      ),
 | 
				
			||||||
      (
 | 
					      (
 | 
				
			||||||
        NavigationDrawerDestination(
 | 
					        NavigationDrawerDestination(
 | 
				
			||||||
          icon: const Icon(Icons.account_circle),
 | 
					          icon: const Icon(Icons.account_circle),
 | 
				
			||||||
          label: Text(AppLocalizations.of(context)!.account),
 | 
					          label: Text(AppLocalizations.of(context)!.account),
 | 
				
			||||||
        ),
 | 
					        ),
 | 
				
			||||||
        "account",
 | 
					        'account',
 | 
				
			||||||
      ),
 | 
					      ),
 | 
				
			||||||
    ];
 | 
					    ];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -69,7 +69,7 @@ class _SolianNavigationDrawerState extends State<SolianNavigationDrawer> {
 | 
				
			|||||||
          child: Row(
 | 
					          child: Row(
 | 
				
			||||||
            mainAxisAlignment: MainAxisAlignment.start,
 | 
					            mainAxisAlignment: MainAxisAlignment.start,
 | 
				
			||||||
            children: [
 | 
					            children: [
 | 
				
			||||||
              Image.asset("assets/logo.png", width: 26, height: 26),
 | 
					              Image.asset('assets/logo.png', width: 26, height: 26),
 | 
				
			||||||
              const SizedBox(width: 10),
 | 
					              const SizedBox(width: 10),
 | 
				
			||||||
              Text(
 | 
					              Text(
 | 
				
			||||||
                AppLocalizations.of(context)!.appName,
 | 
					                AppLocalizations.of(context)!.appName,
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -85,7 +85,7 @@ class _NotificationButtonState extends State<NotificationButton> {
 | 
				
			|||||||
      child: IconButton(
 | 
					      child: IconButton(
 | 
				
			||||||
        icon: const Icon(Icons.notifications),
 | 
					        icon: const Icon(Icons.notifications),
 | 
				
			||||||
        onPressed: () {
 | 
					        onPressed: () {
 | 
				
			||||||
          router.pushNamed("notification");
 | 
					          router.pushNamed('notification');
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
      ),
 | 
					      ),
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -112,7 +112,7 @@ class _AttachmentEditorState extends State<AttachmentEditor> {
 | 
				
			|||||||
    var res = await auth.client!.send(req);
 | 
					    var res = await auth.client!.send(req);
 | 
				
			||||||
    if (res.statusCode == 200) {
 | 
					    if (res.statusCode == 200) {
 | 
				
			||||||
      var result = Attachment.fromJson(
 | 
					      var result = Attachment.fromJson(
 | 
				
			||||||
        jsonDecode(utf8.decode(await res.stream.toBytes()))["info"],
 | 
					        jsonDecode(utf8.decode(await res.stream.toBytes()))['info'],
 | 
				
			||||||
      );
 | 
					      );
 | 
				
			||||||
      setState(() => _attachments.add(result));
 | 
					      setState(() => _attachments.add(result));
 | 
				
			||||||
      widget.onUpdate(_attachments);
 | 
					      widget.onUpdate(_attachments);
 | 
				
			||||||
@@ -252,7 +252,7 @@ class _AttachmentEditorState extends State<AttachmentEditor> {
 | 
				
			|||||||
                            style: Theme.of(context).textTheme.titleMedium,
 | 
					                            style: Theme.of(context).textTheme.titleMedium,
 | 
				
			||||||
                          ),
 | 
					                          ),
 | 
				
			||||||
                          Text(
 | 
					                          Text(
 | 
				
			||||||
                            "${getFileType(element)} · ${formatBytes(element.filesize)}",
 | 
					                            '${getFileType(element)} · ${formatBytes(element.filesize)}',
 | 
				
			||||||
                          ),
 | 
					                          ),
 | 
				
			||||||
                        ],
 | 
					                        ],
 | 
				
			||||||
                      ),
 | 
					                      ),
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -121,7 +121,7 @@ class CommentListHeader extends StatelessWidget {
 | 
				
			|||||||
                return TextButton(
 | 
					                return TextButton(
 | 
				
			||||||
                  onPressed: () async {
 | 
					                  onPressed: () async {
 | 
				
			||||||
                    final did = await router.pushNamed(
 | 
					                    final did = await router.pushNamed(
 | 
				
			||||||
                      "posts.comments.editor",
 | 
					                      'posts.comments.editor',
 | 
				
			||||||
                      extra: CommentPostArguments(related: related),
 | 
					                      extra: CommentPostArguments(related: related),
 | 
				
			||||||
                    );
 | 
					                    );
 | 
				
			||||||
                    if (did == true) paging.refresh();
 | 
					                    if (did == true) paging.refresh();
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -55,7 +55,7 @@ class ArticleContent extends StatelessWidget {
 | 
				
			|||||||
                },
 | 
					                },
 | 
				
			||||||
                imageBuilder: (url, _, __) {
 | 
					                imageBuilder: (url, _, __) {
 | 
				
			||||||
                  Uri uri;
 | 
					                  Uri uri;
 | 
				
			||||||
                  if (url.toString().startsWith("/api/attachments")) {
 | 
					                  if (url.toString().startsWith('/api/attachments')) {
 | 
				
			||||||
                    uri = getRequestUri('interactive', url.toString());
 | 
					                    uri = getRequestUri('interactive', url.toString());
 | 
				
			||||||
                  } else {
 | 
					                  } else {
 | 
				
			||||||
                    uri = url;
 | 
					                    uri = url;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -30,7 +30,7 @@ class _AttachmentItemState extends State<AttachmentItem> {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  late final _videoPlayer = Player(
 | 
					  late final _videoPlayer = Player(
 | 
				
			||||||
    configuration: PlayerConfiguration(
 | 
					    configuration: PlayerConfiguration(
 | 
				
			||||||
      title: "Attachment #${getTag()}",
 | 
					      title: 'Attachment #${getTag()}',
 | 
				
			||||||
      logLevel: MPVLogLevel.error,
 | 
					      logLevel: MPVLogLevel.error,
 | 
				
			||||||
    ),
 | 
					    ),
 | 
				
			||||||
  );
 | 
					  );
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -13,8 +13,8 @@ import 'package:timeago/timeago.dart' as timeago;
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
class PostItem extends StatefulWidget {
 | 
					class PostItem extends StatefulWidget {
 | 
				
			||||||
  final Post item;
 | 
					  final Post item;
 | 
				
			||||||
  final bool? brief;
 | 
					  final bool brief;
 | 
				
			||||||
  final bool? ripple;
 | 
					  final bool ripple;
 | 
				
			||||||
  final Function? onUpdate;
 | 
					  final Function? onUpdate;
 | 
				
			||||||
  final Function? onDelete;
 | 
					  final Function? onDelete;
 | 
				
			||||||
  final Function? onTap;
 | 
					  final Function? onTap;
 | 
				
			||||||
@@ -22,8 +22,8 @@ class PostItem extends StatefulWidget {
 | 
				
			|||||||
  const PostItem({
 | 
					  const PostItem({
 | 
				
			||||||
    super.key,
 | 
					    super.key,
 | 
				
			||||||
    required this.item,
 | 
					    required this.item,
 | 
				
			||||||
    this.brief,
 | 
					    this.brief = true,
 | 
				
			||||||
    this.ripple,
 | 
					    this.ripple = true,
 | 
				
			||||||
    this.onUpdate,
 | 
					    this.onUpdate,
 | 
				
			||||||
    this.onDelete,
 | 
					    this.onDelete,
 | 
				
			||||||
    this.onTap,
 | 
					    this.onTap,
 | 
				
			||||||
@@ -79,9 +79,9 @@ class _PostItemState extends State<PostItem> {
 | 
				
			|||||||
  Widget renderContent() {
 | 
					  Widget renderContent() {
 | 
				
			||||||
    switch (widget.item.modelType) {
 | 
					    switch (widget.item.modelType) {
 | 
				
			||||||
      case 'article':
 | 
					      case 'article':
 | 
				
			||||||
        return ArticleContent(item: widget.item, brief: widget.brief ?? true);
 | 
					        return ArticleContent(item: widget.item, brief: widget.brief);
 | 
				
			||||||
      default:
 | 
					      default:
 | 
				
			||||||
        return MomentContent(item: widget.item, brief: widget.brief ?? true);
 | 
					        return MomentContent(item: widget.item, brief: widget.brief);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -163,7 +163,7 @@ class _PostItemState extends State<PostItem> {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    Widget content;
 | 
					    Widget content;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (widget.brief ?? true) {
 | 
					    if (widget.brief) {
 | 
				
			||||||
      content = Padding(
 | 
					      content = Padding(
 | 
				
			||||||
        padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 16),
 | 
					        padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 16),
 | 
				
			||||||
        child: Column(
 | 
					        child: Column(
 | 
				
			||||||
@@ -199,7 +199,7 @@ class _PostItemState extends State<PostItem> {
 | 
				
			|||||||
      content = Column(
 | 
					      content = Column(
 | 
				
			||||||
        children: [
 | 
					        children: [
 | 
				
			||||||
          Padding(
 | 
					          Padding(
 | 
				
			||||||
            padding: const EdgeInsets.only(left: 12, right: 12, top: 16),
 | 
					            padding: const EdgeInsets.only(left: 20, right: 20, top: 16),
 | 
				
			||||||
            child: Row(
 | 
					            child: Row(
 | 
				
			||||||
              crossAxisAlignment: CrossAxisAlignment.start,
 | 
					              crossAxisAlignment: CrossAxisAlignment.start,
 | 
				
			||||||
              children: [
 | 
					              children: [
 | 
				
			||||||
@@ -230,17 +230,17 @@ class _PostItemState extends State<PostItem> {
 | 
				
			|||||||
            child: Divider(thickness: 0.3),
 | 
					            child: Divider(thickness: 0.3),
 | 
				
			||||||
          ),
 | 
					          ),
 | 
				
			||||||
          Padding(
 | 
					          Padding(
 | 
				
			||||||
            padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
 | 
					            padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 8),
 | 
				
			||||||
            child: renderContent(),
 | 
					            child: renderContent(),
 | 
				
			||||||
          ),
 | 
					          ),
 | 
				
			||||||
          Padding(
 | 
					          Padding(
 | 
				
			||||||
            padding: const EdgeInsets.symmetric(horizontal: 8),
 | 
					            padding: const EdgeInsets.symmetric(horizontal: 16),
 | 
				
			||||||
            child: renderAttachments(),
 | 
					            child: renderAttachments(),
 | 
				
			||||||
          ),
 | 
					          ),
 | 
				
			||||||
          ClipRRect(
 | 
					          ClipRRect(
 | 
				
			||||||
            borderRadius: BorderRadius.circular(8),
 | 
					            borderRadius: BorderRadius.circular(8),
 | 
				
			||||||
            child: Padding(
 | 
					            child: Padding(
 | 
				
			||||||
              padding: const EdgeInsets.symmetric(horizontal: 8),
 | 
					              padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 2),
 | 
				
			||||||
              child: renderReactions(),
 | 
					              child: renderReactions(),
 | 
				
			||||||
            ),
 | 
					            ),
 | 
				
			||||||
          ),
 | 
					          ),
 | 
				
			||||||
@@ -248,9 +248,7 @@ class _PostItemState extends State<PostItem> {
 | 
				
			|||||||
      );
 | 
					      );
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    final ripple = widget.ripple ?? true;
 | 
					    if (widget.ripple) {
 | 
				
			||||||
 | 
					 | 
				
			||||||
    if (ripple) {
 | 
					 | 
				
			||||||
      return InkWell(
 | 
					      return InkWell(
 | 
				
			||||||
        child: content,
 | 
					        child: content,
 | 
				
			||||||
        onTap: () {
 | 
					        onTap: () {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -134,8 +134,8 @@ class _ReactionActionPopupState extends State<ReactionActionPopup> {
 | 
				
			|||||||
                child: ListTile(
 | 
					                child: ListTile(
 | 
				
			||||||
                  title: Text(info.value.icon),
 | 
					                  title: Text(info.value.icon),
 | 
				
			||||||
                  subtitle: Text(
 | 
					                  subtitle: Text(
 | 
				
			||||||
                    ":${info.key}:",
 | 
					                    ':${info.key}:',
 | 
				
			||||||
                    style: const TextStyle(fontFamily: "monospace"),
 | 
					                    style: const TextStyle(fontFamily: 'monospace'),
 | 
				
			||||||
                  ),
 | 
					                  ),
 | 
				
			||||||
                ),
 | 
					                ),
 | 
				
			||||||
              );
 | 
					              );
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user