use super::*;
impl AppCore {
pub(super) fn create_group(&mut self, name: &str, member_inputs: &[String]) {
self.create_group_inner(name, member_inputs, None);
}
pub(super) fn create_group_with_picture(
&mut self,
name: &str,
member_inputs: &[String],
_picture_file_path: &str,
_picture_filename: &str,
) {
self.create_group_inner(name, member_inputs, None);
}
fn create_group_inner(
&mut self,
name: &str,
member_inputs: &[String],
_picture: Option<(String, String)>,
) {
if self.logged_in.is_none() {
self.state.toast = Some("Create or restore a profile first.".to_string());
self.emit_state();
return;
}
if !self.can_use_chats() {
self.state.toast = Some(chat_unavailable_message(self.logged_in.as_ref()).to_string());
self.emit_state();
return;
}
let trimmed_name = name.trim();
if trimmed_name.is_empty() {
self.state.toast = Some("Group name is required.".to_string());
self.emit_state();
return;
}
let Some(local_owner) = self
.logged_in
.as_ref()
.map(|logged_in| logged_in.owner_pubkey)
else {
return;
};
let member_owners = match parse_owner_inputs(member_inputs, local_owner) {
Ok(member_owners) => member_owners,
Err(error) => {
self.state.toast = Some(error.to_string());
self.emit_state();
return;
}
};
self.state.busy.creating_group = true;
self.emit_state();
let now = unix_now();
let result = self
.logged_in
.as_ref()
.expect("checked above")
.ndr_runtime
.create_group(trimmed_name.to_string(), member_owners.clone());
match result {
Ok(result) => {
for owner in member_owners {
if let Some(logged_in) = self.logged_in.as_ref() {
let _ = logged_in.ndr_runtime.setup_user(owner);
}
}
let group = result.group;
let chat_id = group_chat_id(&group.group_id);
self.apply_group_snapshot_to_threads(&group, now.get());
self.groups.insert(group.group_id.clone(), group.clone());
self.active_chat_id = Some(chat_id.clone());
self.screen_stack = vec![Screen::Chat { chat_id }];
self.apply_group_metadata_notice(None, &group);
self.process_runtime_events();
self.request_protocol_subscription_refresh();
self.schedule_tracked_peer_catch_up(Duration::from_secs(
RESUBSCRIBE_CATCH_UP_DELAY_SECS,
));
}
Err(error) => self.state.toast = Some(error.to_string()),
}
self.state.busy.creating_group = false;
self.rebuild_state();
self.persist_best_effort();
self.emit_state();
}
pub(super) fn update_group_name(&mut self, group_id: &str, name: &str) {
let trimmed = name.trim();
if trimmed.is_empty() {
self.state.toast = Some("Group name is required.".to_string());
self.emit_state();
return;
}
self.state.busy.updating_group = true;
self.emit_state();
let previous = self.groups.get(group_id).cloned();
let result = self.logged_in.as_ref().map(|logged_in| {
logged_in
.ndr_runtime
.update_group_name(group_id, trimmed.to_string())
});
match result {
Some(Ok(group)) => {
self.apply_local_group_snapshot(previous.as_ref(), group, "group.rename")
}
Some(Err(error)) => self.state.toast = Some(error.to_string()),
None => {}
}
self.state.busy.updating_group = false;
self.rebuild_state();
self.persist_best_effort();
self.emit_state();
}
pub(super) fn update_group_picture(
&mut self,
_group_id: &str,
_file_path: &str,
_filename: &str,
) {
self.state.toast = Some(
"Group photos are not supported on the experimental group protocol yet.".to_string(),
);
self.emit_state();
}
pub(super) fn add_group_members(&mut self, group_id: &str, member_inputs: &[String]) {
let Some(local_owner) = self
.logged_in
.as_ref()
.map(|logged_in| logged_in.owner_pubkey)
else {
return;
};
let member_owners = match parse_owner_inputs(member_inputs, local_owner) {
Ok(member_owners) if !member_owners.is_empty() => member_owners,
Ok(_) => return,
Err(error) => {
self.state.toast = Some(error.to_string());
self.emit_state();
return;
}
};
self.state.busy.updating_group = true;
self.emit_state();
let previous = self.groups.get(group_id).cloned();
let result = self
.logged_in
.as_ref()
.expect("checked above")
.ndr_runtime
.add_group_members(group_id, member_owners.clone());
match result {
Ok(group) => {
for owner in member_owners {
if let Some(logged_in) = self.logged_in.as_ref() {
let _ = logged_in.ndr_runtime.setup_user(owner);
}
}
self.apply_local_group_snapshot(previous.as_ref(), group, "group.add_members");
}
Err(error) => self.state.toast = Some(error.to_string()),
}
self.state.busy.updating_group = false;
self.rebuild_state();
self.persist_best_effort();
self.emit_state();
}
pub(super) fn set_group_admin(
&mut self,
group_id: &str,
owner_pubkey_hex: &str,
is_admin: bool,
) {
let Ok(owner) = parse_owner_input(owner_pubkey_hex) else {
self.state.toast = Some("Invalid member key.".to_string());
self.emit_state();
return;
};
let previous = self.groups.get(group_id).cloned();
let result = self
.logged_in
.as_ref()
.expect("checked above")
.ndr_runtime
.set_group_admin(group_id, owner, is_admin);
match result {
Ok(group) => self.apply_local_group_snapshot(
previous.as_ref(),
group,
if is_admin {
"group.add_admin"
} else {
"group.remove_admin"
},
),
Err(error) => self.state.toast = Some(error.to_string()),
}
self.rebuild_state();
self.persist_best_effort();
self.emit_state();
}
pub(super) fn remove_group_member(&mut self, group_id: &str, owner_pubkey_hex: &str) {
let Ok(owner) = parse_owner_input(owner_pubkey_hex) else {
self.state.toast = Some("Invalid member key.".to_string());
self.emit_state();
return;
};
let previous = self.groups.get(group_id).cloned();
let result = self
.logged_in
.as_ref()
.expect("checked above")
.ndr_runtime
.remove_group_member(group_id, owner);
match result {
Ok(group) => {
self.apply_local_group_snapshot(previous.as_ref(), group, "group.remove_member")
}
Err(error) => self.state.toast = Some(error.to_string()),
}
self.rebuild_state();
self.persist_best_effort();
self.emit_state();
}
fn apply_local_group_snapshot(
&mut self,
previous: Option<&GroupSnapshot>,
group: GroupSnapshot,
debug_category: &'static str,
) {
self.groups.insert(group.group_id.clone(), group.clone());
self.apply_group_snapshot_to_threads(&group, unix_now().get());
self.push_debug_log(debug_category, group.group_id.clone());
self.apply_group_metadata_notice(previous, &group);
self.process_runtime_events();
self.request_protocol_subscription_refresh();
}
pub(super) fn apply_group_snapshot_to_threads(
&mut self,
group: &GroupSnapshot,
updated_at_secs: u64,
) {
self.ensure_thread_record(&group_chat_id(&group.group_id), updated_at_secs);
}
pub(super) fn apply_group_decrypted_event(&mut self, event: GroupIncomingEvent) {
self.mark_mobile_push_dirty();
match event {
GroupIncomingEvent::MetadataUpdated(group) => {
let previous = self.groups.get(&group.group_id).cloned();
self.groups.insert(group.group_id.clone(), group.clone());
self.apply_group_snapshot_to_threads(
&group,
unix_now().get().max(group.updated_at.get()),
);
self.apply_group_metadata_notice(previous.as_ref(), &group);
}
GroupIncomingEvent::Message(message) => {
let chat_id = group_chat_id(&message.group_id);
let payload = decode_app_group_message_payload(&message.body);
let body = payload
.map(|payload| payload.body)
.unwrap_or_else(|| String::from_utf8_lossy(&message.body).to_string());
let sender_owner = PublicKey::from_slice(&message.sender_owner.to_bytes())
.expect("owner pubkey bytes must be valid");
let is_outgoing = self
.logged_in
.as_ref()
.map(|logged_in| sender_owner == logged_in.owner_pubkey)
.unwrap_or(false);
self.apply_runtime_text_message(
sender_owner,
Some(chat_id),
body,
unix_now().get(),
None,
None,
None,
);
if is_outgoing {
self.request_protocol_subscription_refresh();
}
}
}
}
pub(super) fn apply_group_metadata_notice(
&mut self,
previous: Option<&GroupSnapshot>,
group: &GroupSnapshot,
) {
let chat_id = group_chat_id(&group.group_id);
let now = unix_now().get();
match previous {
None => {
self.push_system_notice(&chat_id, format!("Group created: {}", group.name), now)
}
Some(previous) => {
if previous.name != group.name {
self.push_system_notice(
&chat_id,
format!("Group renamed to {}", group.name),
now,
);
}
for owner in group
.members
.iter()
.filter(|owner| !previous.members.iter().any(|existing| existing == *owner))
{
self.push_system_notice(
&chat_id,
format!(
"{} joined the group",
self.owner_display_label(&owner.to_string())
),
now,
);
}
for owner in previous
.members
.iter()
.filter(|owner| !group.members.iter().any(|existing| existing == *owner))
{
self.push_system_notice(
&chat_id,
format!(
"{} left the group",
self.owner_display_label(&owner.to_string())
),
now,
);
}
if previous.admins != group.admins {
self.push_system_notice(
&chat_id,
self.admin_change_notice(previous, group),
now,
);
}
}
}
}
pub(super) fn sync_runtime_groups(&mut self) {}
fn admin_change_notice(&self, previous: &GroupSnapshot, group: &GroupSnapshot) -> String {
let added = group
.admins
.iter()
.find(|admin| !previous.admins.iter().any(|existing| existing == *admin));
if let Some(owner) = added {
return format!(
"{} became an admin",
self.owner_display_label(&owner.to_string())
);
}
let removed = previous
.admins
.iter()
.find(|admin| !group.admins.iter().any(|existing| existing == *admin));
if let Some(owner) = removed {
return format!(
"{} is no longer an admin",
self.owner_display_label(&owner.to_string())
);
}
"Group admins changed".to_string()
}
}