mirror of
https://github.com/youwen5/iamb.git
synced 2025-06-19 21:29:52 -07:00
Support sending and displaying message reactions (#2)
This commit is contained in:
parent
3629f15e0d
commit
c9c547acc1
10 changed files with 368 additions and 63 deletions
40
Cargo.lock
generated
40
Cargo.lock
generated
|
@ -752,6 +752,15 @@ version = "1.8.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "90e5c1c8368803113bf0c9584fc495a58b86dc8a29edbf8fe877d21d9507e797"
|
||||
|
||||
[[package]]
|
||||
name = "emojis"
|
||||
version = "0.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "44fe60b864b6544ad211d4053ced474a9b9d2c8d66b77f01d6c6bcfed10c6bf0"
|
||||
dependencies = [
|
||||
"phf 0.11.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "encoding_rs"
|
||||
version = "0.8.31"
|
||||
|
@ -1165,6 +1174,7 @@ dependencies = [
|
|||
"clap",
|
||||
"css-color-parser",
|
||||
"dirs",
|
||||
"emojis",
|
||||
"gethostname",
|
||||
"html5ever",
|
||||
"lazy_static 1.4.0",
|
||||
|
@ -1434,7 +1444,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "7a2629bb1404f3d34c2e921f21fd34ba00b206124c81f65c50b43b6aaefeb016"
|
||||
dependencies = [
|
||||
"log",
|
||||
"phf",
|
||||
"phf 0.10.1",
|
||||
"phf_codegen",
|
||||
"string_cache",
|
||||
"string_cache_codegen",
|
||||
|
@ -1884,7 +1894,16 @@ version = "0.10.1"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fabbf1ead8a5bcbc20f5f8b939ee3f5b0f6f281b6ad3468b84656b658b455259"
|
||||
dependencies = [
|
||||
"phf_shared",
|
||||
"phf_shared 0.10.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "phf"
|
||||
version = "0.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "928c6535de93548188ef63bb7c4036bd415cd8f36ad25af44b9789b2ee72a48c"
|
||||
dependencies = [
|
||||
"phf_shared 0.11.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -1894,7 +1913,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "4fb1c3a8bc4dd4e5cfce29b44ffc14bedd2ee294559a294e2a4d4c9e9a6a13cd"
|
||||
dependencies = [
|
||||
"phf_generator",
|
||||
"phf_shared",
|
||||
"phf_shared 0.10.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -1903,7 +1922,7 @@ version = "0.10.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5d5285893bb5eb82e6aaf5d59ee909a06a16737a8970984dd7746ba9283498d6"
|
||||
dependencies = [
|
||||
"phf_shared",
|
||||
"phf_shared 0.10.0",
|
||||
"rand 0.8.5",
|
||||
]
|
||||
|
||||
|
@ -1916,6 +1935,15 @@ dependencies = [
|
|||
"siphasher",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "phf_shared"
|
||||
version = "0.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e1fb5f6f826b772a8d4c0394209441e7d37cbbb967ae9c7e0e8134365c9ee676"
|
||||
dependencies = [
|
||||
"siphasher",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pin-project"
|
||||
version = "1.0.12"
|
||||
|
@ -2637,7 +2665,7 @@ dependencies = [
|
|||
"new_debug_unreachable",
|
||||
"once_cell",
|
||||
"parking_lot 0.12.1",
|
||||
"phf_shared",
|
||||
"phf_shared 0.10.0",
|
||||
"precomputed-hash",
|
||||
"serde",
|
||||
]
|
||||
|
@ -2649,7 +2677,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "6bb30289b722be4ff74a408c3cc27edeaad656e06cb1fe8fa9231fa59c728988"
|
||||
dependencies = [
|
||||
"phf_generator",
|
||||
"phf_shared",
|
||||
"phf_shared 0.10.0",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
]
|
||||
|
|
|
@ -19,6 +19,7 @@ chrono = "0.4"
|
|||
clap = {version = "4.0", features = ["derive"]}
|
||||
css-color-parser = "0.1.2"
|
||||
dirs = "4.0.0"
|
||||
emojis = "~0.5.2"
|
||||
gethostname = "0.4.1"
|
||||
html5ever = "0.26.0"
|
||||
markup5ever_rcdom = "0.2.0"
|
||||
|
|
125
src/base.rs
125
src/base.rs
|
@ -10,14 +10,19 @@ use matrix_sdk::{
|
|||
encryption::verification::SasVerification,
|
||||
room::Joined,
|
||||
ruma::{
|
||||
events::room::message::{
|
||||
OriginalRoomMessageEvent,
|
||||
Relation,
|
||||
Replacement,
|
||||
RoomMessageEvent,
|
||||
RoomMessageEventContent,
|
||||
events::{
|
||||
reaction::ReactionEvent,
|
||||
room::message::{
|
||||
OriginalRoomMessageEvent,
|
||||
Relation,
|
||||
Replacement,
|
||||
RoomMessageEvent,
|
||||
RoomMessageEventContent,
|
||||
},
|
||||
tag::{TagName, Tags},
|
||||
AnyMessageLikeEvent,
|
||||
MessageLikeEvent,
|
||||
},
|
||||
events::tag::{TagName, Tags},
|
||||
EventId,
|
||||
OwnedEventId,
|
||||
OwnedRoomId,
|
||||
|
@ -88,11 +93,20 @@ pub enum MessageAction {
|
|||
/// Edit a sent message.
|
||||
Edit,
|
||||
|
||||
/// Redact a message.
|
||||
/// React to a message with an Emoji.
|
||||
React(String),
|
||||
|
||||
/// Redact a message, with an optional reason.
|
||||
Redact(Option<String>),
|
||||
|
||||
/// Reply to a message.
|
||||
Reply,
|
||||
|
||||
/// Unreact to a message.
|
||||
///
|
||||
/// If no specific Emoji to remove to is specified, then all reactions from the user on the
|
||||
/// message are removed.
|
||||
Unreact(Option<String>),
|
||||
}
|
||||
|
||||
bitflags::bitflags! {
|
||||
|
@ -226,6 +240,12 @@ pub type AsyncProgramStore = Arc<AsyncMutex<ProgramStore>>;
|
|||
|
||||
pub type IambResult<T> = UIResult<T, IambInfo>;
|
||||
|
||||
/// Reaction events for some message.
|
||||
///
|
||||
/// The event identifier used as a key here is the ID for the reaction, and not for the message
|
||||
/// it's reacting to.
|
||||
pub type MessageReactions = HashMap<OwnedEventId, (String, OwnedUserId)>;
|
||||
|
||||
pub type Receipts = HashMap<OwnedEventId, Vec<OwnedUserId>>;
|
||||
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
|
@ -292,32 +312,103 @@ pub enum RoomFetchStatus {
|
|||
NotStarted,
|
||||
}
|
||||
|
||||
pub enum EventLocation {
|
||||
Message(MessageKey),
|
||||
Reaction(OwnedEventId),
|
||||
}
|
||||
|
||||
impl EventLocation {
|
||||
fn to_message_key(&self) -> Option<&MessageKey> {
|
||||
if let EventLocation::Message(key) = self {
|
||||
Some(key)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct RoomInfo {
|
||||
/// The display name for this room.
|
||||
pub name: Option<String>,
|
||||
|
||||
/// The tags placed on this room.
|
||||
pub tags: Option<Tags>,
|
||||
|
||||
pub keys: HashMap<OwnedEventId, MessageKey>,
|
||||
/// A map of event IDs to where they are stored in this struct.
|
||||
pub keys: HashMap<OwnedEventId, EventLocation>,
|
||||
|
||||
/// The messages loaded for this room.
|
||||
pub messages: Messages,
|
||||
|
||||
/// A map of read markers to display on different events.
|
||||
pub receipts: HashMap<OwnedEventId, Vec<OwnedUserId>>,
|
||||
|
||||
/// An event ID for where we should indicate we've read up to.
|
||||
pub read_till: Option<OwnedEventId>,
|
||||
|
||||
/// A map of message identifiers to a map of reaction events.
|
||||
pub reactions: HashMap<OwnedEventId, MessageReactions>,
|
||||
|
||||
/// Where to continue fetching from when we continue loading scrollback history.
|
||||
pub fetch_id: RoomFetchStatus,
|
||||
|
||||
/// The time that we last fetched scrollback for this room.
|
||||
pub fetch_last: Option<Instant>,
|
||||
|
||||
/// Users currently typing in this room, and when we received notification of them doing so.
|
||||
pub users_typing: Option<(Instant, Vec<OwnedUserId>)>,
|
||||
}
|
||||
|
||||
impl RoomInfo {
|
||||
pub fn get_reactions(&self, event_id: &EventId) -> Vec<(&str, usize)> {
|
||||
if let Some(reacts) = self.reactions.get(event_id) {
|
||||
let mut counts = HashMap::new();
|
||||
|
||||
for (key, _) in reacts.values() {
|
||||
let count = counts.entry(key.as_str()).or_default();
|
||||
*count += 1;
|
||||
}
|
||||
|
||||
let mut reactions = counts.into_iter().collect::<Vec<_>>();
|
||||
reactions.sort();
|
||||
|
||||
reactions
|
||||
} else {
|
||||
vec![]
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_event(&self, event_id: &EventId) -> Option<&Message> {
|
||||
self.messages.get(self.keys.get(event_id)?)
|
||||
self.messages.get(self.keys.get(event_id)?.to_message_key()?)
|
||||
}
|
||||
|
||||
pub fn insert_reaction(&mut self, react: ReactionEvent) {
|
||||
match react {
|
||||
MessageLikeEvent::Original(react) => {
|
||||
let rel_id = react.content.relates_to.event_id;
|
||||
let key = react.content.relates_to.key;
|
||||
|
||||
let message = self.reactions.entry(rel_id.clone()).or_default();
|
||||
let event_id = react.event_id;
|
||||
let user_id = react.sender;
|
||||
|
||||
message.insert(event_id.clone(), (key, user_id));
|
||||
|
||||
let loc = EventLocation::Reaction(rel_id);
|
||||
self.keys.insert(event_id, loc);
|
||||
},
|
||||
MessageLikeEvent::Redacted(_) => {
|
||||
return;
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn insert_edit(&mut self, msg: Replacement) {
|
||||
let event_id = msg.event_id;
|
||||
let new_content = msg.new_content;
|
||||
|
||||
let key = if let Some(k) = self.keys.get(&event_id) {
|
||||
let key = if let Some(EventLocation::Message(k)) = self.keys.get(&event_id) {
|
||||
k
|
||||
} else {
|
||||
return;
|
||||
|
@ -346,7 +437,7 @@ impl RoomInfo {
|
|||
let event_id = msg.event_id().to_owned();
|
||||
let key = (msg.origin_server_ts().into(), event_id.clone());
|
||||
|
||||
self.keys.insert(event_id.clone(), key.clone());
|
||||
self.keys.insert(event_id.clone(), EventLocation::Message(key.clone()));
|
||||
self.messages.insert(key, msg.into());
|
||||
|
||||
// Remove any echo.
|
||||
|
@ -519,7 +610,15 @@ impl ChatStore {
|
|||
match res {
|
||||
Ok((fetch_id, msgs)) => {
|
||||
for msg in msgs.into_iter() {
|
||||
info.insert(msg);
|
||||
match msg {
|
||||
AnyMessageLikeEvent::RoomMessage(msg) => {
|
||||
info.insert(msg);
|
||||
},
|
||||
AnyMessageLikeEvent::Reaction(ev) => {
|
||||
info.insert_reaction(ev);
|
||||
},
|
||||
_ => continue,
|
||||
}
|
||||
}
|
||||
|
||||
info.fetch_id =
|
||||
|
|
|
@ -163,8 +163,8 @@ fn iamb_cancel(desc: CommandDescription, ctx: &mut ProgContext) -> ProgResult {
|
|||
return Result::Err(CommandError::InvalidArgument);
|
||||
}
|
||||
|
||||
let ract = IambAction::from(MessageAction::Cancel);
|
||||
let step = CommandStep::Continue(ract.into(), ctx.context.take());
|
||||
let mact = IambAction::from(MessageAction::Cancel);
|
||||
let step = CommandStep::Continue(mact.into(), ctx.context.take());
|
||||
|
||||
return Ok(step);
|
||||
}
|
||||
|
@ -174,8 +174,55 @@ fn iamb_edit(desc: CommandDescription, ctx: &mut ProgContext) -> ProgResult {
|
|||
return Result::Err(CommandError::InvalidArgument);
|
||||
}
|
||||
|
||||
let ract = IambAction::from(MessageAction::Edit);
|
||||
let step = CommandStep::Continue(ract.into(), ctx.context.take());
|
||||
let mact = IambAction::from(MessageAction::Edit);
|
||||
let step = CommandStep::Continue(mact.into(), ctx.context.take());
|
||||
|
||||
return Ok(step);
|
||||
}
|
||||
|
||||
fn iamb_react(desc: CommandDescription, ctx: &mut ProgContext) -> ProgResult {
|
||||
let args = desc.arg.strings()?;
|
||||
|
||||
if args.len() != 1 {
|
||||
return Result::Err(CommandError::InvalidArgument);
|
||||
}
|
||||
|
||||
let k = args[0].as_str();
|
||||
|
||||
if let Some(emoji) = emojis::get(k).or_else(|| emojis::get_by_shortcode(k)) {
|
||||
let mact = IambAction::from(MessageAction::React(emoji.to_string()));
|
||||
let step = CommandStep::Continue(mact.into(), ctx.context.take());
|
||||
|
||||
return Ok(step);
|
||||
} else {
|
||||
let msg = format!("Invalid Emoji or shortcode: {k}");
|
||||
|
||||
return Result::Err(CommandError::Error(msg));
|
||||
}
|
||||
}
|
||||
|
||||
fn iamb_unreact(desc: CommandDescription, ctx: &mut ProgContext) -> ProgResult {
|
||||
let mut args = desc.arg.strings()?;
|
||||
|
||||
if args.len() > 1 {
|
||||
return Result::Err(CommandError::InvalidArgument);
|
||||
}
|
||||
|
||||
let mact = if let Some(k) = args.pop() {
|
||||
let k = k.as_str();
|
||||
|
||||
if let Some(emoji) = emojis::get(k).or_else(|| emojis::get_by_shortcode(k)) {
|
||||
IambAction::from(MessageAction::Unreact(Some(emoji.to_string())))
|
||||
} else {
|
||||
let msg = format!("Invalid Emoji or shortcode: {k}");
|
||||
|
||||
return Result::Err(CommandError::Error(msg));
|
||||
}
|
||||
} else {
|
||||
IambAction::from(MessageAction::Unreact(None))
|
||||
};
|
||||
|
||||
let step = CommandStep::Continue(mact.into(), ctx.context.take());
|
||||
|
||||
return Ok(step);
|
||||
}
|
||||
|
@ -356,11 +403,13 @@ fn add_iamb_commands(cmds: &mut ProgramCommands) {
|
|||
cmds.add_command(ProgramCommand { names: vec!["invite".into()], f: iamb_invite });
|
||||
cmds.add_command(ProgramCommand { names: vec!["join".into()], f: iamb_join });
|
||||
cmds.add_command(ProgramCommand { names: vec!["members".into()], f: iamb_members });
|
||||
cmds.add_command(ProgramCommand { names: vec!["react".into()], f: iamb_react });
|
||||
cmds.add_command(ProgramCommand { names: vec!["redact".into()], f: iamb_redact });
|
||||
cmds.add_command(ProgramCommand { names: vec!["reply".into()], f: iamb_reply });
|
||||
cmds.add_command(ProgramCommand { names: vec!["rooms".into()], f: iamb_rooms });
|
||||
cmds.add_command(ProgramCommand { names: vec!["room".into()], f: iamb_room });
|
||||
cmds.add_command(ProgramCommand { names: vec!["spaces".into()], f: iamb_spaces });
|
||||
cmds.add_command(ProgramCommand { names: vec!["unreact".into()], f: iamb_unreact });
|
||||
cmds.add_command(ProgramCommand { names: vec!["upload".into()], f: iamb_upload });
|
||||
cmds.add_command(ProgramCommand { names: vec!["verify".into()], f: iamb_verify });
|
||||
cmds.add_command(ProgramCommand { names: vec!["welcome".into()], f: iamb_welcome });
|
||||
|
|
|
@ -178,6 +178,8 @@ fn merge_users(a: Option<UserOverrides>, b: Option<UserOverrides>) -> Option<Use
|
|||
|
||||
#[derive(Clone)]
|
||||
pub struct TunableValues {
|
||||
pub reaction_display: bool,
|
||||
pub reaction_shortcode_display: bool,
|
||||
pub read_receipt_send: bool,
|
||||
pub read_receipt_display: bool,
|
||||
pub typing_notice_send: bool,
|
||||
|
@ -188,6 +190,8 @@ pub struct TunableValues {
|
|||
|
||||
#[derive(Clone, Default, Deserialize)]
|
||||
pub struct Tunables {
|
||||
pub reaction_display: Option<bool>,
|
||||
pub reaction_shortcode_display: Option<bool>,
|
||||
pub read_receipt_send: Option<bool>,
|
||||
pub read_receipt_display: Option<bool>,
|
||||
pub typing_notice_send: Option<bool>,
|
||||
|
@ -199,6 +203,10 @@ pub struct Tunables {
|
|||
impl Tunables {
|
||||
fn merge(self, other: Self) -> Self {
|
||||
Tunables {
|
||||
reaction_display: self.reaction_display.or(other.reaction_display),
|
||||
reaction_shortcode_display: self
|
||||
.reaction_shortcode_display
|
||||
.or(other.reaction_shortcode_display),
|
||||
read_receipt_send: self.read_receipt_send.or(other.read_receipt_send),
|
||||
read_receipt_display: self.read_receipt_display.or(other.read_receipt_display),
|
||||
typing_notice_send: self.typing_notice_send.or(other.typing_notice_send),
|
||||
|
@ -210,6 +218,8 @@ impl Tunables {
|
|||
|
||||
fn values(self) -> TunableValues {
|
||||
TunableValues {
|
||||
reaction_display: self.reaction_display.unwrap_or(true),
|
||||
reaction_shortcode_display: self.reaction_shortcode_display.unwrap_or(false),
|
||||
read_receipt_send: self.read_receipt_send.unwrap_or(true),
|
||||
read_receipt_display: self.read_receipt_display.unwrap_or(true),
|
||||
typing_notice_send: self.typing_notice_send.unwrap_or(true),
|
||||
|
|
|
@ -24,6 +24,7 @@ use matrix_sdk::ruma::{
|
|||
},
|
||||
redaction::SyncRoomRedactionEvent,
|
||||
},
|
||||
AnyMessageLikeEvent,
|
||||
Redact,
|
||||
},
|
||||
EventId,
|
||||
|
@ -52,7 +53,7 @@ use crate::{
|
|||
mod html;
|
||||
mod printer;
|
||||
|
||||
pub type MessageFetchResult = IambResult<(Option<String>, Vec<RoomMessageEvent>)>;
|
||||
pub type MessageFetchResult = IambResult<(Option<String>, Vec<AnyMessageLikeEvent>)>;
|
||||
pub type MessageKey = (MessageTimeStamp, OwnedEventId);
|
||||
pub type Messages = BTreeMap<MessageKey, Message>;
|
||||
|
||||
|
@ -682,6 +683,43 @@ impl Message {
|
|||
fmt.push_spans(space_span(width, style).into(), style, &mut text);
|
||||
}
|
||||
|
||||
if settings.tunables.reaction_display {
|
||||
let mut emojis = printer::TextPrinter::new(width, style, false);
|
||||
let mut reactions = 0;
|
||||
|
||||
for (key, count) in info.get_reactions(self.event.event_id()).into_iter() {
|
||||
if reactions != 0 {
|
||||
emojis.push_str(" ", style);
|
||||
}
|
||||
|
||||
let name = if settings.tunables.reaction_shortcode_display {
|
||||
if let Some(emoji) = emojis::get(key) {
|
||||
if let Some(short) = emoji.shortcode() {
|
||||
short
|
||||
} else {
|
||||
// No ASCII shortcode name to show.
|
||||
continue;
|
||||
}
|
||||
} else if key.chars().all(|c| c.is_ascii_alphanumeric()) {
|
||||
key
|
||||
} else {
|
||||
// Not an Emoji or a printable ASCII string.
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
key
|
||||
};
|
||||
|
||||
emojis.push_str(format!("[{name} {count}]"), style);
|
||||
|
||||
reactions += 1;
|
||||
}
|
||||
|
||||
if reactions > 0 {
|
||||
fmt.push_text(emojis.finish(), style, &mut text);
|
||||
}
|
||||
}
|
||||
|
||||
return text;
|
||||
}
|
||||
|
||||
|
|
17
src/tests.rs
17
src/tests.rs
|
@ -20,7 +20,7 @@ use tokio::sync::mpsc::unbounded_channel;
|
|||
use url::Url;
|
||||
|
||||
use crate::{
|
||||
base::{ChatStore, ProgramStore, RoomFetchStatus, RoomInfo},
|
||||
base::{ChatStore, EventLocation, ProgramStore, RoomFetchStatus, RoomInfo},
|
||||
config::{
|
||||
user_color,
|
||||
user_style_from_color,
|
||||
|
@ -117,14 +117,14 @@ pub fn mock_message5() -> Message {
|
|||
mock_room1_message(content, TEST_USER2.clone(), MSG4_KEY.clone())
|
||||
}
|
||||
|
||||
pub fn mock_keys() -> HashMap<OwnedEventId, MessageKey> {
|
||||
pub fn mock_keys() -> HashMap<OwnedEventId, EventLocation> {
|
||||
let mut keys = HashMap::new();
|
||||
|
||||
keys.insert(MSG1_EVID.clone(), MSG1_KEY.clone());
|
||||
keys.insert(MSG2_EVID.clone(), MSG2_KEY.clone());
|
||||
keys.insert(MSG3_EVID.clone(), MSG3_KEY.clone());
|
||||
keys.insert(MSG4_EVID.clone(), MSG4_KEY.clone());
|
||||
keys.insert(MSG5_EVID.clone(), MSG5_KEY.clone());
|
||||
keys.insert(MSG1_EVID.clone(), EventLocation::Message(MSG1_KEY.clone()));
|
||||
keys.insert(MSG2_EVID.clone(), EventLocation::Message(MSG2_KEY.clone()));
|
||||
keys.insert(MSG3_EVID.clone(), EventLocation::Message(MSG3_KEY.clone()));
|
||||
keys.insert(MSG4_EVID.clone(), EventLocation::Message(MSG4_KEY.clone()));
|
||||
keys.insert(MSG5_EVID.clone(), EventLocation::Message(MSG5_KEY.clone()));
|
||||
|
||||
keys
|
||||
}
|
||||
|
@ -151,6 +151,7 @@ pub fn mock_room() -> RoomInfo {
|
|||
|
||||
receipts: HashMap::new(),
|
||||
read_till: None,
|
||||
reactions: HashMap::new(),
|
||||
|
||||
fetch_id: RoomFetchStatus::NotStarted,
|
||||
fetch_last: None,
|
||||
|
@ -169,6 +170,8 @@ pub fn mock_dirs() -> DirectoryValues {
|
|||
pub fn mock_tunables() -> TunableValues {
|
||||
TunableValues {
|
||||
default_room: None,
|
||||
reaction_display: true,
|
||||
reaction_shortcode_display: false,
|
||||
read_receipt_send: true,
|
||||
read_receipt_display: true,
|
||||
typing_notice_send: true,
|
||||
|
|
|
@ -9,8 +9,9 @@ use tokio;
|
|||
use matrix_sdk::{
|
||||
attachment::AttachmentConfig,
|
||||
media::{MediaFormat, MediaRequest},
|
||||
room::Room as MatrixRoom,
|
||||
room::{Joined, Room as MatrixRoom},
|
||||
ruma::{
|
||||
events::reaction::{ReactionEventContent, Relation as Reaction},
|
||||
events::room::message::{
|
||||
MessageType,
|
||||
OriginalRoomMessageEvent,
|
||||
|
@ -19,6 +20,7 @@ use matrix_sdk::{
|
|||
RoomMessageEventContent,
|
||||
TextMessageEventContent,
|
||||
},
|
||||
EventId,
|
||||
OwnedRoomId,
|
||||
RoomId,
|
||||
},
|
||||
|
@ -73,6 +75,7 @@ use crate::base::{
|
|||
};
|
||||
|
||||
use crate::message::{Message, MessageEvent, MessageKey, MessageTimeStamp};
|
||||
use crate::worker::Requester;
|
||||
|
||||
use super::scrollback::{Scrollback, ScrollbackState};
|
||||
|
||||
|
@ -115,6 +118,10 @@ impl ChatState {
|
|||
}
|
||||
}
|
||||
|
||||
fn get_joined(&self, worker: &Requester) -> Result<Joined, IambError> {
|
||||
worker.client.get_joined_room(self.id()).ok_or(IambError::NotJoined)
|
||||
}
|
||||
|
||||
fn get_reply_to<'a>(&self, info: &'a RoomInfo) -> Option<&'a OriginalRoomMessageEvent> {
|
||||
let key = self.reply_to.as_ref()?;
|
||||
let msg = info.messages.get(key)?;
|
||||
|
@ -149,7 +156,10 @@ impl ChatState {
|
|||
let settings = &store.application.settings;
|
||||
let info = store.application.rooms.entry(self.room_id.clone()).or_default();
|
||||
|
||||
let msg = self.scrollback.get_mut(info).ok_or(IambError::NoSelectedMessage)?;
|
||||
let msg = self
|
||||
.scrollback
|
||||
.get_mut(&mut info.messages)
|
||||
.ok_or(IambError::NoSelectedMessage)?;
|
||||
|
||||
match act {
|
||||
MessageAction::Cancel => {
|
||||
|
@ -282,19 +292,32 @@ impl ChatState {
|
|||
|
||||
Ok(None)
|
||||
},
|
||||
MessageAction::Redact(reason) => {
|
||||
let room = store
|
||||
.application
|
||||
.worker
|
||||
.client
|
||||
.get_joined_room(self.id())
|
||||
.ok_or(IambError::NotJoined)?;
|
||||
|
||||
MessageAction::React(emoji) => {
|
||||
let room = self.get_joined(&store.application.worker)?;
|
||||
let event_id = match &msg.event {
|
||||
MessageEvent::Original(ev) => ev.event_id.clone(),
|
||||
MessageEvent::Local(event_id, _) => event_id.clone(),
|
||||
MessageEvent::Redacted(_) => {
|
||||
let msg = "";
|
||||
let msg = "Cannot react to a redacted message";
|
||||
let err = UIError::Failure(msg.into());
|
||||
|
||||
return Err(err);
|
||||
},
|
||||
};
|
||||
|
||||
let reaction = Reaction::new(event_id, emoji);
|
||||
let msg = ReactionEventContent::new(reaction);
|
||||
let _ = room.send(msg, None).await.map_err(IambError::from)?;
|
||||
|
||||
Ok(None)
|
||||
},
|
||||
MessageAction::Redact(reason) => {
|
||||
let room = self.get_joined(&store.application.worker)?;
|
||||
let event_id = match &msg.event {
|
||||
MessageEvent::Original(ev) => ev.event_id.clone(),
|
||||
MessageEvent::Local(event_id, _) => event_id.clone(),
|
||||
MessageEvent::Redacted(_) => {
|
||||
let msg = "Cannot redact already redacted message";
|
||||
let err = UIError::Failure(msg.into());
|
||||
|
||||
return Err(err);
|
||||
|
@ -311,6 +334,46 @@ impl ChatState {
|
|||
self.reply_to = self.scrollback.get_key(info);
|
||||
self.focus = RoomFocus::MessageBar;
|
||||
|
||||
Ok(None)
|
||||
},
|
||||
MessageAction::Unreact(emoji) => {
|
||||
let room = self.get_joined(&store.application.worker)?;
|
||||
let event_id: &EventId = match &msg.event {
|
||||
MessageEvent::Original(ev) => ev.event_id.as_ref(),
|
||||
MessageEvent::Local(event_id, _) => event_id.as_ref(),
|
||||
MessageEvent::Redacted(_) => {
|
||||
let msg = "Cannot unreact to a redacted message";
|
||||
let err = UIError::Failure(msg.into());
|
||||
|
||||
return Err(err);
|
||||
},
|
||||
};
|
||||
|
||||
let reactions = match info.reactions.get(event_id) {
|
||||
Some(r) => r,
|
||||
None => return Ok(None),
|
||||
};
|
||||
|
||||
let reactions = reactions.iter().filter_map(|(event_id, (reaction, user_id))| {
|
||||
if user_id != &settings.profile.user_id {
|
||||
return None;
|
||||
}
|
||||
|
||||
if let Some(emoji) = &emoji {
|
||||
if emoji == reaction {
|
||||
return Some(event_id);
|
||||
} else {
|
||||
return None;
|
||||
}
|
||||
} else {
|
||||
return Some(event_id);
|
||||
}
|
||||
});
|
||||
|
||||
for reaction in reactions {
|
||||
let _ = room.redact(reaction, None, None).await.map_err(IambError::from)?;
|
||||
}
|
||||
|
||||
Ok(None)
|
||||
},
|
||||
}
|
||||
|
|
|
@ -62,7 +62,7 @@ use modalkit::editing::{
|
|||
use crate::{
|
||||
base::{IambBufferId, IambInfo, ProgramContext, ProgramStore, RoomFocus, RoomInfo},
|
||||
config::ApplicationSettings,
|
||||
message::{Message, MessageCursor, MessageKey},
|
||||
message::{Message, MessageCursor, MessageKey, Messages},
|
||||
};
|
||||
|
||||
fn nth_key_before(pos: MessageKey, n: usize, info: &RoomInfo) -> MessageKey {
|
||||
|
@ -164,11 +164,11 @@ impl ScrollbackState {
|
|||
.or_else(|| info.messages.last_key_value().map(|kv| kv.0.clone()))
|
||||
}
|
||||
|
||||
pub fn get_mut<'a>(&mut self, info: &'a mut RoomInfo) -> Option<&'a mut Message> {
|
||||
pub fn get_mut<'a>(&mut self, messages: &'a mut Messages) -> Option<&'a mut Message> {
|
||||
if let Some(k) = &self.cursor.timestamp {
|
||||
info.messages.get_mut(k)
|
||||
messages.get_mut(k)
|
||||
} else {
|
||||
info.messages.last_entry().map(|o| o.into_mut())
|
||||
messages.last_entry().map(|o| o.into_mut())
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -32,6 +32,7 @@ use matrix_sdk::{
|
|||
start::{OriginalSyncKeyVerificationStartEvent, ToDeviceKeyVerificationStartEvent},
|
||||
VerificationMethod,
|
||||
},
|
||||
reaction::ReactionEventContent,
|
||||
room::{
|
||||
message::{MessageType, RoomMessageEventContent},
|
||||
name::RoomNameEventContent,
|
||||
|
@ -39,7 +40,6 @@ use matrix_sdk::{
|
|||
},
|
||||
tag::Tags,
|
||||
typing::SyncTypingEvent,
|
||||
AnyMessageLikeEvent,
|
||||
AnyTimelineEvent,
|
||||
SyncMessageLikeEvent,
|
||||
SyncStateEvent,
|
||||
|
@ -57,7 +57,7 @@ use matrix_sdk::{
|
|||
use modalkit::editing::action::{EditInfo, InfoMessage, UIError};
|
||||
|
||||
use crate::{
|
||||
base::{AsyncProgramStore, IambError, IambResult, Receipts, VerifyAction},
|
||||
base::{AsyncProgramStore, EventLocation, IambError, IambResult, Receipts, VerifyAction},
|
||||
message::MessageFetchResult,
|
||||
ApplicationSettings,
|
||||
};
|
||||
|
@ -552,6 +552,20 @@ impl ClientWorker {
|
|||
},
|
||||
);
|
||||
|
||||
let _ = self.client.add_event_handler(
|
||||
|ev: SyncMessageLikeEvent<ReactionEventContent>,
|
||||
room: MatrixRoom,
|
||||
store: Ctx<AsyncProgramStore>| {
|
||||
async move {
|
||||
let room_id = room.room_id();
|
||||
|
||||
let mut locked = store.lock().await;
|
||||
let info = locked.application.get_room_info(room_id.to_owned());
|
||||
info.insert_reaction(ev.into_full_event(room_id.to_owned()));
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
let _ = self.client.add_event_handler(
|
||||
|ev: OriginalSyncRoomRedactionEvent,
|
||||
room: MatrixRoom,
|
||||
|
@ -564,15 +578,21 @@ impl ClientWorker {
|
|||
let mut locked = store.lock().await;
|
||||
let info = locked.application.get_room_info(room_id.to_owned());
|
||||
|
||||
let key = if let Some(k) = info.keys.get(&ev.redacts) {
|
||||
k
|
||||
} else {
|
||||
return;
|
||||
};
|
||||
match info.keys.get(&ev.redacts) {
|
||||
None => return,
|
||||
Some(EventLocation::Message(key)) => {
|
||||
if let Some(msg) = info.messages.get_mut(key) {
|
||||
let ev = SyncRoomRedactionEvent::Original(ev);
|
||||
msg.event.redact(ev, room_version);
|
||||
}
|
||||
},
|
||||
Some(EventLocation::Reaction(event_id)) => {
|
||||
if let Some(reactions) = info.reactions.get_mut(event_id) {
|
||||
reactions.remove(&ev.redacts);
|
||||
}
|
||||
|
||||
if let Some(msg) = info.messages.get_mut(key) {
|
||||
let ev = SyncRoomRedactionEvent::Original(ev);
|
||||
msg.event.redact(ev, room_version);
|
||||
info.keys.remove(&ev.redacts);
|
||||
},
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -891,13 +911,7 @@ impl ClientWorker {
|
|||
|
||||
let msgs = chunk.into_iter().filter_map(|ev| {
|
||||
match ev.event.deserialize() {
|
||||
Ok(AnyTimelineEvent::MessageLike(msg)) => {
|
||||
if let AnyMessageLikeEvent::RoomMessage(msg) = msg {
|
||||
Some(msg)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
},
|
||||
Ok(AnyTimelineEvent::MessageLike(msg)) => Some(msg),
|
||||
Ok(AnyTimelineEvent::State(_)) => None,
|
||||
Err(_) => None,
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue