Radish alpha
r
rad:z4D5UCArafTzTQpDZNQRuqswh3ury
Radicle desktop app
Radicle
Git
Convert cob actions into customized desktop actions
Merged did:key:z6MkkfM3...sVz5 opened 1 year ago

This commit allows us to e.g. convert the Set of DIDs we obtain from assignee actions on cobs into a Set of Authors with their Aliases.

Also it introduces the FromRadicleAction trait which can be implemented for any type of enum we get from the radicle create to convert into a type which is better suited for us.

Removes also any calling the alias command in the PatchTimeline or IssueTimeline since we already get the alias from the backend the first time now.

Also does the following:

  • Remove unused logging statements
  • Change some eprintln statements for log::error

This partially solves issue 337f765acff2fac32ea4c5ea26e0852c7bc6bddc

checkcheck-unit-testcheck-e2e

👉 Workflow runs 👉 Branch on GitHub

18 files changed +283 -92 47ea7d78 c730e821
modified crates/radicle-tauri/src/commands/cob/issue.rs
@@ -1,7 +1,7 @@
use radicle::git;
use radicle::identity;

-
use radicle::issue::{Action, TYPENAME};
+
use radicle::issue::TYPENAME;
use radicle_types as types;
use radicle_types::error::Error;
use radicle_types::traits::cobs::Cobs;
@@ -63,6 +63,6 @@ pub fn activity_by_issue(
    ctx: tauri::State<AppState>,
    rid: identity::RepoId,
    id: git::Oid,
-
) -> Result<Vec<types::cobs::Operation<Action>>, Error> {
+
) -> Result<Vec<types::cobs::Operation<types::cobs::issue::Action>>, Error> {
    ctx.activity_by_id(rid, &TYPENAME, id)
}
modified crates/radicle-tauri/src/commands/cob/patch.rs
@@ -1,6 +1,6 @@
use radicle::git;
use radicle::identity;
-
use radicle::patch::{Action, TYPENAME};
+
use radicle::patch::TYPENAME;

use radicle_types as types;
use radicle_types::cobs;
@@ -95,6 +95,6 @@ pub fn activity_by_patch(
    ctx: tauri::State<AppState>,
    rid: identity::RepoId,
    id: git::Oid,
-
) -> Result<Vec<types::cobs::Operation<Action>>, Error> {
+
) -> Result<Vec<types::cobs::Operation<models::patch::Action>>, Error> {
    ctx.activity_by_id(rid, &TYPENAME, id)
}
modified crates/radicle-types/bindings/cob/issue/Action.ts
@@ -1,9 +1,10 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
+
import type { Author } from "../Author";
import type { Embed } from "../thread/Embed";
import type { State } from "./State";

export type Action =
-
  | { "type": "assign"; assignees: Array<string> }
+
  | { "type": "assign"; assignees: Array<Author> }
  | { "type": "edit"; title: string }
  | { "type": "lifecycle"; state: State }
  | { "type": "label"; labels: Array<string> }
modified crates/radicle-types/bindings/cob/patch/Action.ts
@@ -1,4 +1,5 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
+
import type { Author } from "../Author";
import type { CodeLocation } from "../thread/CodeLocation";
import type { Embed } from "../thread/Embed";
import type { Verdict } from "./Verdict";
@@ -7,7 +8,7 @@ export type Action =
  | { "type": "edit"; title: string; target: string }
  | { "type": "label"; labels: Array<string> }
  | { "type": "lifecycle"; state: { status: "draft" | "open" | "archived" } }
-
  | { "type": "assign"; assignees: Array<string> }
+
  | { "type": "assign"; assignees: Array<Author> }
  | { "type": "merge"; revision: string; commit: string }
  | {
    "type": "review";
modified crates/radicle-types/src/cobs.rs
@@ -1,3 +1,4 @@
+
use radicle::profile::Aliases;
use serde::{Deserialize, Serialize};
use ts_rs::TS;

@@ -11,7 +12,7 @@ pub mod repo;
pub mod stream;
pub mod thread;

-
#[derive(Debug, Clone, Serialize, TS, Deserialize)]
+
#[derive(Debug, Clone, Serialize, TS, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
#[serde(rename_all = "camelCase")]
#[ts(export)]
#[ts(export_to = "cob/")]
@@ -30,6 +31,14 @@ impl Author {
            alias: aliases.alias(did),
        }
    }
+

+
    pub fn did(&self) -> &identity::Did {
+
        &self.did
+
    }
+
}
+

+
pub trait FromRadicleAction<A> {
+
    fn from_radicle_action(value: A, aliases: &Aliases) -> Self;
}

/// Everything that can be done in the system is represented by an `Op`.
modified crates/radicle-types/src/cobs/issue.rs
@@ -10,6 +10,9 @@ use radicle::issue;

use crate::cobs;

+
use super::Author;
+
use super::FromRadicleAction;
+

#[derive(TS, Serialize)]
#[ts(export)]
#[ts(export_to = "cob/issue/")]
@@ -136,10 +139,7 @@ pub struct NewIssue {
#[ts(export_to = "cob/issue/")]
pub enum Action {
    #[serde(rename = "assign")]
-
    Assign {
-
        #[ts(as = "Vec<String>")]
-
        assignees: BTreeSet<identity::Did>,
-
    },
+
    Assign { assignees: BTreeSet<Author> },

    #[serde(rename = "edit")]
    Edit { title: String },
@@ -189,3 +189,48 @@ pub enum Action {
        active: bool,
    },
}
+

+
impl FromRadicleAction<radicle::issue::Action> for Action {
+
    fn from_radicle_action(
+
        value: radicle::issue::Action,
+
        aliases: &radicle::profile::Aliases,
+
    ) -> Self {
+
        match value {
+
            radicle::issue::Action::Assign { assignees } => Self::Assign {
+
                assignees: assignees
+
                    .iter()
+
                    .map(|a| Author::new(a, aliases))
+
                    .collect::<BTreeSet<_>>(),
+
            },
+
            radicle::issue::Action::Comment {
+
                body,
+
                reply_to,
+
                embeds,
+
            } => Self::Comment {
+
                body,
+
                reply_to,
+
                embeds: embeds.into_iter().map(Into::into).collect::<Vec<_>>(),
+
            },
+
            radicle::issue::Action::CommentEdit { id, body, embeds } => Self::CommentEdit {
+
                id,
+
                body,
+
                embeds: embeds.into_iter().map(Into::into).collect::<Vec<_>>(),
+
            },
+
            radicle::issue::Action::CommentReact {
+
                id,
+
                reaction,
+
                active,
+
            } => Self::CommentReact {
+
                id,
+
                reaction,
+
                active,
+
            },
+
            radicle::issue::Action::CommentRedact { id } => Self::CommentRedact { id },
+
            radicle::issue::Action::Label { labels } => Self::Label { labels },
+
            radicle::issue::Action::Lifecycle { state } => Self::Lifecycle {
+
                state: state.into(),
+
            },
+
            radicle::issue::Action::Edit { title } => Self::Edit { title },
+
        }
+
    }
+
}
modified crates/radicle-types/src/cobs/stream/iter.rs
@@ -217,8 +217,6 @@ where
                                }
                                Err(err) => return Some(Err(err)),
                            }
-
                            log::trace!(target: "radicle", "Iterating over commit {}", commit.id());
-
                            log::trace!(target: "radicle", "Iterating over tree {}", tree.id());

                            let entry = self.repo.load(commit.id().into()).ok()?;
                            let op = Op::from(entry);
@@ -338,7 +336,6 @@ fn action<A>(
where
    A: for<'de> Deserialize<'de>,
{
-
    log::trace!(target: "radicle", "Deserializing action {}", blob.id());
    let action = json::from_slice::<A>(blob.content())
        .map_err(|err| error::Action::new(blob.id().into(), err))?;

modified crates/radicle-types/src/domain/patch/models/patch.rs
@@ -2,15 +2,17 @@ use std::collections::BTreeMap;
use std::collections::BTreeSet;

use radicle::node::AliasStore;
+
use radicle::profile::Aliases;
use serde::{Deserialize, Serialize};
use ts_rs::TS;

use radicle::cob;
use radicle::git;
-
use radicle::identity;
use radicle::patch;

use crate::cobs;
+
use crate::cobs::Author;
+
use crate::cobs::FromRadicleAction;

#[derive(Debug, TS, Serialize)]
#[ts(export)]
@@ -329,10 +331,7 @@ pub enum Action {
        state: patch::Lifecycle,
    },
    #[serde(rename = "assign")]
-
    Assign {
-
        #[ts(as = "Vec<String>")]
-
        assignees: BTreeSet<identity::Did>,
-
    },
+
    Assign { assignees: BTreeSet<cobs::Author> },
    #[serde(rename = "merge")]
    Merge {
        #[ts(as = "String")]
@@ -512,3 +511,160 @@ pub enum Action {
        active: bool,
    },
}
+

+
impl FromRadicleAction<radicle::patch::Action> for Action {
+
    fn from_radicle_action(value: radicle::patch::Action, aliases: &Aliases) -> Self {
+
        match value {
+
            radicle::patch::Action::ReviewRedact { review } => Self::ReviewRedact { review },
+
            radicle::patch::Action::RevisionCommentReact {
+
                revision,
+
                comment,
+
                reaction,
+
                active,
+
            } => Self::RevisionCommentReact {
+
                revision,
+
                comment,
+
                reaction,
+
                active,
+
            },
+
            radicle::patch::Action::RevisionCommentRedact { revision, comment } => {
+
                Self::RevisionCommentRedact { revision, comment }
+
            }
+
            radicle::patch::Action::RevisionCommentEdit {
+
                revision,
+
                comment,
+
                body,
+
                embeds,
+
            } => Self::RevisionCommentEdit {
+
                revision,
+
                comment,
+
                body,
+
                embeds: embeds.into_iter().map(Into::into).collect::<Vec<_>>(),
+
            },
+
            radicle::patch::Action::RevisionComment {
+
                revision,
+
                location,
+
                body,
+
                reply_to,
+
                embeds,
+
            } => Self::RevisionComment {
+
                revision,
+
                location: location.map(Into::into),
+
                body,
+
                reply_to,
+
                embeds: embeds.into_iter().map(Into::into).collect::<Vec<_>>(),
+
            },
+
            radicle::patch::Action::RevisionRedact { revision } => {
+
                Self::RevisionRedact { revision }
+
            }
+
            radicle::patch::Action::RevisionReact {
+
                revision,
+
                location,
+
                reaction,
+
                active,
+
            } => Self::RevisionReact {
+
                revision,
+
                location: location.map(Into::into),
+
                reaction,
+
                active,
+
            },
+
            radicle::patch::Action::RevisionEdit {
+
                revision,
+
                description,
+
                embeds,
+
            } => Self::RevisionEdit {
+
                revision,
+
                description,
+
                embeds: embeds.into_iter().map(Into::into).collect::<Vec<_>>(),
+
            },
+
            radicle::patch::Action::Assign { assignees } => Self::Assign {
+
                assignees: assignees
+
                    .iter()
+
                    .map(|a| Author::new(a, aliases))
+
                    .collect::<BTreeSet<_>>(),
+
            },
+
            radicle::patch::Action::Edit { title, target } => Self::Edit { title, target },
+
            radicle::patch::Action::Label { labels } => Self::Label { labels },
+
            radicle::patch::Action::Lifecycle { state } => Self::Lifecycle { state },
+
            radicle::patch::Action::Merge { revision, commit } => Self::Merge { revision, commit },
+
            radicle::patch::Action::Revision {
+
                description,
+
                base,
+
                oid,
+
                resolves,
+
            } => Self::Revision {
+
                description,
+
                base,
+
                oid,
+
                resolves,
+
            },
+

+
            radicle::patch::Action::Review {
+
                revision,
+
                summary,
+
                verdict,
+
                labels,
+
            } => Self::Review {
+
                revision,
+
                summary,
+
                verdict: verdict.map(Into::into),
+
                labels,
+
            },
+
            radicle::patch::Action::ReviewComment {
+
                review,
+
                body,
+
                location,
+
                reply_to,
+
                embeds,
+
            } => Self::ReviewComment {
+
                review,
+
                body,
+
                location: location.map(Into::into),
+
                reply_to,
+
                embeds: embeds.into_iter().map(Into::into).collect::<Vec<_>>(),
+
            },
+
            radicle::patch::Action::ReviewCommentEdit {
+
                review,
+
                comment,
+
                body,
+
                embeds,
+
            } => Self::ReviewCommentEdit {
+
                review,
+
                comment,
+
                body,
+
                embeds: embeds.into_iter().map(Into::into).collect::<Vec<_>>(),
+
            },
+
            radicle::patch::Action::ReviewCommentReact {
+
                review,
+
                comment,
+
                reaction,
+
                active,
+
            } => Self::ReviewCommentReact {
+
                review,
+
                comment,
+
                reaction,
+
                active,
+
            },
+
            radicle::patch::Action::ReviewCommentRedact { review, comment } => {
+
                Self::ReviewCommentRedact { review, comment }
+
            }
+
            radicle::patch::Action::ReviewCommentResolve { review, comment } => {
+
                Self::ReviewCommentResolve { review, comment }
+
            }
+
            radicle::patch::Action::ReviewCommentUnresolve { review, comment } => {
+
                Self::ReviewCommentUnresolve { review, comment }
+
            }
+
            radicle::patch::Action::ReviewEdit {
+
                review,
+
                summary,
+
                verdict,
+
                labels,
+
            } => Self::ReviewEdit {
+
                review,
+
                summary,
+
                verdict: verdict.map(Into::into),
+
                labels,
+
            },
+
        }
+
    }
+
}
modified crates/radicle-types/src/syntax.rs
@@ -188,7 +188,6 @@ impl Builder {
        for event in highlights {
            match event? {
                ts::HighlightEvent::Source { start, end } => {
-
                    println!("highlightEvent::source {start} {end}");
                    for (i, byte) in code.iter().enumerate().skip(start).take(end - start) {
                        if *byte == b'\n' {
                            self.advance();
@@ -208,7 +207,6 @@ impl Builder {
                }
                ts::HighlightEvent::HighlightStart(h) => {
                    let name = HIGHLIGHTS[h.0];
-
                    println!("highlightEvent::HighlightStart {name}");

                    self.advance();
                    self.styles.push(name.to_string());
@@ -282,7 +280,6 @@ impl Highlighter {
            let Ok(code) = std::str::from_utf8(code) else {
                return Err(ts::Error::Unknown);
            };
-
            println!("Not able to detect language?");
            return Ok(code.lines().map(Line::new).collect());
        };

@@ -291,7 +288,6 @@ impl Highlighter {
            let Ok(code) = std::str::from_utf8(code) else {
                return Err(ts::Error::Unknown);
            };
-
            println!("Not found a configuration?");
            return Ok(code.lines().map(Line::new).collect());
        };

modified crates/radicle-types/src/traits/cobs.rs
@@ -2,17 +2,18 @@ use radicle::storage::ReadStorage;
use radicle::{cob, git, identity};
use serde::de::DeserializeOwned;

+
use crate::cobs::FromRadicleAction;
use crate::error::Error;
use crate::traits::Profile;

pub trait Cobs: Profile {
    #[allow(clippy::unnecessary_filter_map)]
-
    fn activity_by_id<A: DeserializeOwned>(
+
    fn activity_by_id<A: DeserializeOwned, B: FromRadicleAction<A>>(
        &self,
        rid: identity::RepoId,
        type_name: &cob::TypeName,
        id: git::Oid,
-
    ) -> Result<Vec<crate::cobs::Operation<A>>, Error> {
+
    ) -> Result<Vec<crate::cobs::Operation<B>>, Error> {
        let profile = self.profile();
        let aliases = profile.aliases();
        let repo = profile.storage.repository(rid)?;
@@ -23,7 +24,16 @@ pub trait Cobs: Profile {
                let actions = op
                    .actions
                    .iter()
-
                    .filter_map(|a| serde_json::from_slice(a).ok())
+
                    .filter_map(|a| {
+
                        if let Ok(r) = serde_json::from_slice::<A>(a) {
+
                            let x = B::from_radicle_action(r, &aliases);
+
                            Some(x)
+
                        } else {
+
                            log::error!("Not able to deserialize the action");
+

+
                            None
+
                        }
+
                    })
                    .collect::<Vec<_>>();

                Some(crate::cobs::Operation {
modified crates/radicle-types/src/traits/issue.rs
@@ -1,3 +1,5 @@
+
use std::collections::BTreeSet;
+

use radicle::issue::cache::Issues as _;
use radicle::node::Handle;
use radicle::storage::ReadStorage;
@@ -115,7 +117,7 @@ pub trait IssuesMut: Profile {

        if opts.announce() {
            if let Err(e) = node.announce_refs(rid) {
-
                eprintln!("Not able to announce changes: {}", e)
+
                log::error!("Not able to announce changes: {}", e)
            }
        }

@@ -142,7 +144,10 @@ pub trait IssuesMut: Profile {
                issue.lifecycle(state.into(), &signer)?;
            }
            cobs::issue::Action::Assign { assignees } => {
-
                issue.assign(assignees, &signer)?;
+
                issue.assign(
+
                    assignees.iter().map(|a| *a.did()).collect::<BTreeSet<_>>(),
+
                    &signer,
+
                )?;
            }
            cobs::issue::Action::Label { labels } => {
                issue.label(labels, &signer)?;
@@ -184,7 +189,7 @@ pub trait IssuesMut: Profile {

        if opts.announce() {
            if let Err(e) = node.announce_refs(rid) {
-
                eprintln!("Not able to announce changes: {}", e)
+
                log::error!("Not able to announce changes: {}", e)
            }
        }

modified crates/radicle-types/src/traits/patch.rs
@@ -1,3 +1,5 @@
+
use std::collections::BTreeSet;
+

use radicle::node::Handle;
use radicle::patch::cache::Patches as _;
use radicle::storage::ReadStorage;
@@ -176,7 +178,10 @@ pub trait PatchesMut: Profile {
                patch.lifecycle(state, &signer)?;
            }
            models::patch::Action::Assign { assignees } => {
-
                patch.assign(assignees, &signer)?;
+
                patch.assign(
+
                    assignees.iter().map(|a| *a.did()).collect::<BTreeSet<_>>(),
+
                    &signer,
+
                )?;
            }
            models::patch::Action::Label { labels } => {
                patch.label(labels, &signer)?;
@@ -246,7 +251,7 @@ pub trait PatchesMut: Profile {

        if opts.announce() {
            if let Err(e) = node.announce_refs(rid) {
-
                eprintln!("Not able to announce changes: {}", e)
+
                log::error!("Not able to announce changes: {}", e)
            }
        }

modified crates/radicle-types/src/traits/thread.rs
@@ -110,7 +110,7 @@ pub trait Thread: Profile {

        if opts.announce() {
            if let Err(e) = node.announce_refs(rid) {
-
                eprintln!("Not able to announce changes: {}", e)
+
                log::error!("Not able to announce changes: {}", e)
            }
        }

@@ -153,7 +153,7 @@ pub trait Thread: Profile {

        if opts.announce() {
            if let Err(e) = node.announce_refs(rid) {
-
                eprintln!("Not able to announce changes: {}", e)
+
                log::error!("Not able to announce changes: {}", e)
            }
        }

modified crates/test-http-api/src/api.rs
@@ -13,10 +13,10 @@ use tower_http::cors::{self, CorsLayer};

use radicle::{git, identity};
use radicle_types as types;
-
use radicle_types::cobs;
use radicle_types::cobs::issue;
use radicle_types::cobs::issue::NewIssue;
use radicle_types::cobs::CobOptions;
+
use radicle_types::cobs::{self, FromRadicleAction};
use radicle_types::domain::inbox::models::notification::NotificationCount;
use radicle_types::domain::patch::models;
use radicle_types::domain::patch::service::Service;
@@ -71,11 +71,11 @@ pub fn router(ctx: Context) -> Router {
        .route("/diff_stats", post(diff_stats_handler))
        .route(
            "/activity_by_issue",
-
            post(activity_issue_handler::<issue::Action>),
+
            post(activity_issue_handler::<radicle::issue::Action, issue::Action>),
        )
        .route(
            "/activity_by_patch",
-
            post(activity_patch_handler::<models::patch::Action>),
+
            post(activity_patch_handler::<radicle::patch::Action, models::patch::Action>),
        )
        .route("/get_diff", post(diff_handler))
        .route("/list_issues", post(issues_handler))
@@ -259,20 +259,26 @@ struct ActivityBody {
    pub id: git::Oid,
}

-
async fn activity_issue_handler<A: serde::Serialize + serde::de::DeserializeOwned>(
+
async fn activity_issue_handler<
+
    A: serde::Serialize + serde::de::DeserializeOwned,
+
    B: FromRadicleAction<A> + serde::Serialize,
+
>(
    State(ctx): State<Context>,
    Json(ActivityBody { rid, id }): Json<ActivityBody>,
) -> impl IntoResponse {
-
    let activity = ctx.activity_by_id::<A>(rid, &radicle::cob::issue::TYPENAME, id)?;
+
    let activity = ctx.activity_by_id::<A, B>(rid, &radicle::cob::issue::TYPENAME, id)?;

    Ok::<_, Error>(Json(activity))
}

-
async fn activity_patch_handler<A: serde::Serialize + serde::de::DeserializeOwned>(
+
async fn activity_patch_handler<
+
    A: serde::Serialize + serde::de::DeserializeOwned,
+
    B: FromRadicleAction<A> + serde::Serialize,
+
>(
    State(ctx): State<Context>,
    Json(ActivityBody { rid, id }): Json<ActivityBody>,
) -> impl IntoResponse {
-
    let activity = ctx.activity_by_id::<A>(rid, &radicle::cob::patch::TYPENAME, id)?;
+
    let activity = ctx.activity_by_id::<A, B>(rid, &radicle::cob::patch::TYPENAME, id)?;

    Ok::<_, Error>(Json(activity))
}
modified src/components/IssueTimeline.svelte
@@ -18,11 +18,9 @@
    formatTimestamp,
    issueStatusColor,
    pluralize,
-
    publicKeyFromDid,
  } from "@app/lib/utils";
  import Icon from "./Icon.svelte";
  import NodeId from "./NodeId.svelte";
-
  import { invoke } from "@app/lib/invoke";
  import Id from "./Id.svelte";

  interface Props {
@@ -66,7 +64,7 @@
    return result;
  }

-
  function itemDiff(previousState: string[], newState: string[]) {
+
  function itemDiff<A>(previousState: A[], newState: A[]) {
    const removed = previousState.filter(x => !newState.includes(x));
    const added = newState.filter(x => !previousState.includes(x));
    return { removed, added };
@@ -169,7 +167,7 @@
        <div class="wrapper">
          <NodeId {...authorForNodeId(op.author)} />
          {#if op.previous && op.previous.type === op.type}
-
            {@const changed = itemDiff(
+
            {@const changed = itemDiff<Author>(
              op.previous?.assignees ?? [],
              op.assignees,
            )}
@@ -177,38 +175,20 @@
              {#if changed.added.length}
                assigned
                {#each changed.added as assignee}
-
                  {#await invoke<string | null>( "alias", { nid: publicKeyFromDid(assignee) }, ) then alias}
-
                    <NodeId
-
                      {...authorForNodeId({
-
                        did: assignee,
-
                        alias: alias ?? undefined,
-
                      })} />
-
                  {/await}
+
                  <NodeId {...authorForNodeId(assignee)} />
                {/each}
              {/if}
              {#if changed.removed.length}
                unassigned
                {#each changed.removed as assignee}
-
                  {#await invoke<string | null>( "alias", { nid: publicKeyFromDid(assignee) }, ) then alias}
-
                    <NodeId
-
                      {...authorForNodeId({
-
                        did: assignee,
-
                        alias: alias ?? undefined,
-
                      })} />
-
                  {/await}
+
                  <NodeId {...authorForNodeId(assignee)} />
                {/each}
              {/if}
            {/if}
          {:else}
            assigned
            {#each op.assignees as assignee}
-
              {#await invoke<string | null>( "alias", { nid: publicKeyFromDid(assignee) }, ) then alias}
-
                <NodeId
-
                  {...authorForNodeId({
-
                    did: assignee,
-
                    alias: alias ?? undefined,
-
                  })} />
-
              {/await}
+
              <NodeId {...authorForNodeId(assignee)} />
            {/each}
          {/if}
          <div title={absoluteTimestamp(op.timestamp)}>
modified src/components/PatchTimeline.svelte
@@ -18,12 +18,10 @@
    formatTimestamp,
    patchStatusColor,
    pluralize,
-
    publicKeyFromDid,
  } from "@app/lib/utils";
  import Icon from "./Icon.svelte";
  import Id from "./Id.svelte";
  import NodeId from "./NodeId.svelte";
-
  import { invoke } from "@app/lib/invoke";

  interface Props {
    patchId: string;
@@ -67,7 +65,7 @@
    return result;
  }

-
  function itemDiff(previousState: string[], newState: string[]) {
+
  function itemDiff<A>(previousState: A[], newState: A[]) {
    const removed = previousState.filter(x => !newState.includes(x));
    const added = newState.filter(x => !previousState.includes(x));
    return { removed, added };
@@ -190,44 +188,26 @@
        <div class="wrapper">
          <NodeId {...authorForNodeId(op.author)} />
          {#if op.previous && op.previous.type === op.type}
-
            {@const changed = itemDiff(
+
            {@const changed = itemDiff<Author>(
              op.previous?.assignees ?? [],
              op.assignees,
            )}
            {#if changed.added.length}
              assigned
              {#each changed.added as assignee}
-
                {#await invoke<string | null>( "alias", { nid: publicKeyFromDid(assignee) }, ) then alias}
-
                  <NodeId
-
                    {...authorForNodeId({
-
                      did: assignee,
-
                      alias: alias ?? undefined,
-
                    })} />
-
                {/await}
+
                <NodeId {...authorForNodeId(assignee)} />
              {/each}
            {/if}
            {#if changed.removed.length}
              unassigned
              {#each changed.removed as assignee}
-
                {#await invoke<string | null>( "alias", { nid: publicKeyFromDid(assignee) }, ) then alias}
-
                  <NodeId
-
                    {...authorForNodeId({
-
                      did: assignee,
-
                      alias: alias ?? undefined,
-
                    })} />
-
                {/await}
+
                <NodeId {...authorForNodeId(assignee)} />
              {/each}
            {/if}
          {:else}
            assigned
            {#each op.assignees as assignee}
-
              {#await invoke<string | null>( "alias", { nid: publicKeyFromDid(assignee) }, ) then alias}
-
                <NodeId
-
                  {...authorForNodeId({
-
                    did: assignee,
-
                    alias: alias ?? undefined,
-
                  })} />
-
              {/await}
+
              <NodeId {...authorForNodeId(assignee)} />
            {/each}
          {/if}
          <div title={absoluteTimestamp(op.timestamp)}>
modified src/views/repo/Issue.svelte
@@ -126,7 +126,7 @@
        cobId: issue.id,
        action: {
          type: "assign",
-
          assignees: assignees.map(a => a.did),
+
          assignees,
        },
        opts: { announce: $nodeRunning && $announce },
      });
modified src/views/repo/Patch.svelte
@@ -173,7 +173,7 @@
        cobId: patch.id,
        action: {
          type: "assign",
-
          assignees: assignees.map(a => a.did),
+
          assignees,
        },
        opts: { announce: $nodeRunning && $announce },
      });