diff --git a/CLAUDE.md b/CLAUDE.md index a7cbba9..91b7250 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -231,6 +231,8 @@ supplies: link, participants and status. - **Profiles and search** (`Pages/Profile.razor` at `/account/{id}`, `Pages/Search.razor` at `/search?q=`): a card's author and a notification's account open the profile in the app; "Open original" is the only way to the remote site. + An account that moved says where to (`Account.Moved`, which PrivaPub fills once the new account names the old one), + linking the new account's profile. A card in a fixed-height flex column with `overflow-hidden` needs `flex-none`, or it collapses to nothing. - **Groups** (`Pages/Groups.razor` at `/groups`): the persona's communities and circles (`GroupService`, `/clientapi/group`): made (a circle unless it is a community, with an invitation code and an optional password), joined by invitation and diff --git a/Models/Mastodon/Account.cs b/Models/Mastodon/Account.cs index 5b9f638..abbf3dc 100644 --- a/Models/Mastodon/Account.cs +++ b/Models/Mastodon/Account.cs @@ -25,6 +25,8 @@ public class Account public string LastStatusAt { get; set; } public List Emojis { get; set; } = []; public List Fields { get; set; } = []; + /// The account it moved to (PrivaPub shows it once the new account names this one among its aliases). + public Account Moved { get; set; } /// Whether it is listed in the directory and suggested; null when its server does not say. public bool? Discoverable { get; set; } /// Whether its public posts may be found by full-text search. diff --git a/Pages/Profile.razor b/Pages/Profile.razor index a70396b..4ed0ed7 100644 --- a/Pages/Profile.razor +++ b/Pages/Profile.razor @@ -32,6 +32,14 @@ } else { + @if (account.Moved is { } moved) + { +

+ + @Localizer["{0} has moved to", account.Name] + @("@" + moved.Acct) +

+ }
diff --git a/Resources/AllStrings.it.resx b/Resources/AllStrings.it.resx index 7fbb3a1..e44343e 100644 --- a/Resources/AllStrings.it.resx +++ b/Resources/AllStrings.it.resx @@ -1715,4 +1715,7 @@ Ogni persona di questo accesso se ne va con lui, e tutto ciò che avevano di pubblico, qui e sui server che lo tengono. Non si può annullare. + + {0} si è trasferito su + diff --git a/Resources/AllStrings.resx b/Resources/AllStrings.resx index 701d0fe..d6830d0 100644 --- a/Resources/AllStrings.resx +++ b/Resources/AllStrings.resx @@ -1715,4 +1715,7 @@ Every persona of this login goes with it, and everything public they had, here and on the servers that hold it. It cannot be undone. + + {0} has moved to + diff --git a/tests/e2e/Actions/ProfileTests.cs b/tests/e2e/Actions/ProfileTests.cs index b0b83c0..4788bf5 100644 --- a/tests/e2e/Actions/ProfileTests.cs +++ b/tests/e2e/Actions/ProfileTests.cs @@ -24,6 +24,39 @@ namespace decePubClient.E2E.Actions return candidates.Last(); } + // the newest GoToSocial account that SocialPub's moves scenario moved (scenarios/moves.sh), as PrivaPub knows it + static async Task<(string Old, string New)> Moved() + { + var start = new System.Diagnostics.ProcessStartInfo("podman") { RedirectStandardOutput = true, RedirectStandardError = true }; + foreach (var arg in new[] { "exec", "pasture-mongo", "mongosh", "--quiet", "PrivaPub", "--eval", + "const a = db.ForeignAvatar.find({ActorURI: {$regex: 'gts.test.users.mvold'}, MovedToURL: {$ne: null}}).sort({_id: -1}).limit(1).toArray()[0];" + + "print(a ? a.ActorURI + ' ' + a.MovedToURL : '')" }) + start.ArgumentList.Add(arg); + using var process = System.Diagnostics.Process.Start(start)!; + var output = (await process.StandardOutput.ReadToEndAsync()).Trim(); + await process.WaitForExitAsync(); + var parts = output.Split(' ', StringSplitOptions.RemoveEmptyEntries); + return parts.Length == 2 ? (parts[0], parts[1]) : default; + } + + [Fact] + public async Task A_moved_accounts_profile_says_where_it_went() + { + var (old, moved) = await Moved(); + Assert.SkipWhen(old is null, "no moved account: run SocialPub's tools/pasture/interop.sh moves"); + var persona = Pasture.Personas().First(); + var handle = old.Split("/users/")[1] + "@gts.test"; + await using var session = await SignIn.As(browser, persona); + await Results.Cell("client.render.moved", "gts", handle, async () => + { + await session.Page.GotoAsync($"{Pasture.Site}/search?q={Uri.EscapeDataString("@" + handle)}"); + var banner = session.Page.Locator("[data-moved]"); + await banner.WaitForAsync(new() { Timeout = 30000 }); + await Assertions.Expect(banner).ToContainTextAsync("@" + moved.Split("/users/")[1] + "@gts.test"); + await session.AssertGuards(); + }, session.Page); + } + [Theory] [MemberData(nameof(Origins))] public async Task A_remote_account_found_by_its_handle_is_followed_and_unfollowed_from_its_profile(string platform)