Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chathistory Support #619

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
Open

chathistory Support #619

wants to merge 8 commits into from

Conversation

andymandias
Copy link
Collaborator

A refactoring of the previous chathistory PR. It has all of the same features as before (plus a new configuration option), but the plumbing has been restructured to better match the paradigm set by the draft/read-marker PR.

Features (when chathistory is supported by the server):

  • Uses chathistory to request new history for all joined channels from the server (limited to one request if no history for the channel that exists, 500 messages or the maximum number of messages allowed by the server).
  • Adds button at the start of channel/query buffers to request older history via chathistory.
  • Uses chathistory to determine whether queries were received since last connection to the server, then request the new history for those queries from the server.
  • Configuration option to automatically request older history when scrolling to the top of a channel/query buffer (infinite_scroll).
  • Configuration option to disable chathistory for a server.

I haven't had much time to test it since refactoring, so I may make fixes in this PR as testing continues. But so far it is working as intended for me, has the entirety of my intended functionality for initial support, and is ready for review.

@andymandias andymandias linked an issue Oct 18, 2024 that may be closed by this pull request
Comment on lines +452 to +482
/// Insert the incoming message into the provided vector, sorted
/// on server time
///
/// Deduplication is only checked +/- 1 second around the server time
/// of the incoming message. Either message IDs match, or server times
/// have an exact match + target & content.
pub fn insert_message(messages: &mut Vec<Message>, message: Message) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Didn't we have a commit which used a binary search implementation for inserting messages?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, this is the insert_message from that work we did together, completely unchanged. I was lazy about persisting the commit history while refactoring, so it's all been squashed into one commit.

The one thing this doesn't keep related to that work is the sorting of saved histories on load. In that version of the chathistory PR, I had changed the history storage scheme to no longer be obfuscated (no longer use hashes in naming the files). If histories were not found with the non-hashed storage scheme (where histories can be expected to be sorted already), then it would attempt to load & sort a history from the old, hashed storage scheme. I still like that idea, but it seemed like this PR might have too many changes already, so I omitted it this time around.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ahh my bad, I just totally forgot what the code looked like and didn't realize that was the implementation we landed on :P

Comment on lines 465 to 493
let start = message::fuzz_start_server_time(message.server_time);
let end = message::fuzz_end_server_time(message.server_time);

Copy link
Member

@tarkah tarkah Oct 21, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think these helpers are helpful or necessary. We can add / sub chrono::Duration from DateTime<Utc> and construct it const. So we can add const FUZZ_SECONDS: chrono::Duration = chrono::Duration::seconds(1) to the first line of this function and then let start = message.server_time - FUZZ_SECONDS;.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Aha! Yes, I think I added the functions primarily because I was only aware of the TimeDelta version of changing the time which is considerably more of a mouthful. Secondarily because FUZZ_SECONDS was being shared with the chathistory request. I don't remember why I did that, and while they're not completely unrelated I don't see any reason to keep them in lock-step. I'll add FUZZ_SECONDS here as you suggest, and set a distinct FUZZ_SECONDS inside isupport.rs.

Copy link
Member

@tarkah tarkah left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is looking really good! A lot easier to grok without all the other work that you've now separately landed.

I think there's some opportunity to remove a lot of the chat history logic from the frontend and push it behind the history API on the data layer which I think would be a little nicer. Let me know what you think! It very well be this isn't the way to go but it's just a gut feeling from briefly seeing how much "data logic" exists on the frontend vs just plumbing code / UI logic

Comment on lines 72 to 84
for message_reference_type in message_reference_types {
match message_reference_type {
isupport::MessageReferenceType::MessageId => {
if let Some(id) = &self.id {
return isupport::MessageReference::MessageId(id.clone());
}
}
isupport::MessageReferenceType::Timestamp => {
return isupport::MessageReference::Timestamp(self.timestamp);
}
}
}

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we prioritize ID over timestamp? If so, should we naively be iterating over the incoming isupport array or should we sort it so we always try id first?

Alternatively isupport::MessageReferenceType has an ALL constant we can loop over in the right order, and we check if each one of those exists in the incoming array and if so we do the normal match.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Otherwise if order doesn't matter disregard

Copy link
Collaborator Author

@andymandias andymandias Nov 8, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The chathistory spec says that they are to be provided by the server ordered in (the server's) decreasing preference. I believe we preserve that order. I think the most polite thing to do is respect the server's preference, but I don't believe we are required to do so. My personal opinion is that ID is preferable over timestamp, when available.


impl PartialEq for MessageReferences {
fn eq(&self, other: &Self) -> bool {
self.timestamp.eq(&other.timestamp)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Where do we do these equality checks and why isn't id considered? I'm asking this naively since I haven't come across it in my review yet.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Currently this is only used for some syntactic sugar, in update_partial in data/src/history.rs this (via Ord) allows .max to be used. id isn't considered because for the purpose it's currently being used we only want to know if we have a more recent set of references.

I originally implemented it this way because Ord was utilized elsewhere in the refactor, but that turned out to be unnecessary and was stripped back to just the one use rather than unraveled completely. No particular attachment to it.

use irc::proto;
use chrono::{format::SecondsFormat, DateTime, Utc};
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: Import ordering, chrono should be grouped after std.. Std -> External -> Crate

Comment on lines 18 to 19
use crate::config::buffer::UsernameFormat;
use crate::history::MessageReferences;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems like message::Reference should live in this module, not in history, as this now creates a circular dependency between the two modules

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Originally put it in history::metadata because that's where it lives, inside the Metadata struct. But I can see the rationale for moving it to message. Will do so.

Comment on lines 287 to 288
&& matches!(self.limit, Limit::Top(_))
&& relative_offset == 0.0
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this can be replaced with self.status.is_top(relative_offset)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Aha, thanks! I can't remember why, but I thought it was necessary to check limit as well. I'll switch this.

src/main.rs Outdated
Comment on lines 750 to 929
target,
message_reference,
);
}
ChatHistorySubcommand::Before(target, message_reference, _) => {
log::debug!(
"[{}] received {} messages in {} before {}",
server,
batch_len,
target,
message_reference,
);
}
ChatHistorySubcommand::Between(
target,
start_message_reference,
end_message_reference,
_,
) => {
log::debug!(
"[{}] received {} messages in {} between {} and {}",
server,
batch_len,
target,
start_message_reference,
end_message_reference,
);
}
ChatHistorySubcommand::Targets(
start_message_reference,
end_message_reference,
_,
) => {
log::debug!(
"[{}] received {} targets between {} and {}",
server,
batch_len,
start_message_reference,
end_message_reference,
);
}
}

self.clients.clear_chathistory_request(&server, subcommand.target());
}
data::client::Event::ChatHistoryTarget(target, server_time) => {
let command = dashboard
.load_metadata(server.clone(), target.clone())
.map_or(Task::none(), |command| command.map(Message::Dashboard));

let command = command.chain(Task::done(Message::Dashboard(
dashboard::Message::RequestNewerChatHistory(
server.clone(),
target,
server_time,
),
)));

commands.push(command);
}
data::client::Event::ChatHistoryTargetsReceived(
subcommand,
batch_len,
server_time
) => {
if let ChatHistorySubcommand::Targets(
start_message_reference,
end_message_reference,
_,
) = subcommand {
log::debug!(
"[{}] received {} targets between {} and {}",
server.clone(),
batch_len,
start_message_reference,
end_message_reference,
);
}

let server = server.clone();

commands.push(
dashboard
.channel_joined(server.clone(), channel)
.map(Message::Dashboard),
Task::perform(
async move {
(
server.clone(),
data::client::overwrite_chathistory_targets_timestamp(
server,
server_time,
)
.await,
)
},
move |(server, result)| {
Message::UpdateChatHistoryTargetsTimestamp(server, server_time, result)
}
),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can this history::Manager API encapsulate a lot of this logic? Similar to what we did for returning Future from it's update and it has it's own internal Message type. Then we can simply hook up client Chat History events to methods on history::Manager which can be called -> Future<Output = Message> -> history_manager.update when those tasks resolve.

I don't think we should have complex backend specific logic in main

Comment on lines 42 to 43
Channel(&'a Server, &'a str, Option<ChatHistoryState>),
Query(&'a Server, &'a Nick, Option<ChatHistoryState>),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stuffing this into here seems to be causing us to require fetching this state when it's not even used. Checkout how we're querying for it when we construct Kind when calling scroll_to_backlog, this state isn't used anywhere there.

It seems we only need it during view so instead pass Option<ChatHistoryState> as a separate param there and it's just always None from server & query. This will clean up some code paths

@andymandias
Copy link
Collaborator Author

I think this is in a better place (that ~addresses the current review comments) and is ready for another look, when you have time @tarkah.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Feature request: IRCv3 CHATHISTORY support
2 participants