feat(web): user-facing pending peering subscriptions and outcome notifications

Two new inline sections in the user-facing federation/connections settings
panel: 'Recent peering outcomes' (terminal-state notifications with
Retry-for-approved + Dismiss) and 'Pending peering approvals' (active
subscriber rows the user is waiting on, with Cancel). New WS handlers for
peering_subscription_changed and peering_notification_received refresh the
lists in real-time and surface a transient toast for online users. Retry
deep-link for friend_add prefills the friend-add input with the original
target handle (other reasons get Dismiss only — the gate doesn't wire
those paths yet).
This commit is contained in:
Jannis Braun
2026-04-26 22:42:53 +02:00
parent eddf2254cc
commit cebbd5c859
4 changed files with 479 additions and 4 deletions
@@ -7,6 +7,7 @@ import { mapServerErrorToMessage } from '../../utils/friendErrors';
import { useSpaceStore } from '../../stores/spaceStore';
import { useInstanceStore } from '../../stores/instanceStore';
import { useUIStore } from '../../stores/uiStore';
import { useFederationStore } from '../../stores/federationStore';
import { Avatar } from '../ui/Avatar';
import { MemberListToggleButton } from '../layout/MemberListToggleButton';
import { LoadingSpinner } from '../ui/LoadingSpinner';
@@ -34,6 +35,17 @@ export function FriendsPage({ mobile }: FriendsPageProps) {
const navigate = useNavigate();
const addDmChannel = useSpaceStore((s) => s.addDmChannel);
// If the user clicked "Retry your friend request" in the Connections panel
// and we just navigated here, the federation store carries the original
// target. Switching to the Add tab makes the AddFriendTab mount, which then
// consumes the prefill into its query input. We only check on mount — the
// store value is one-shot (cleared by AddFriendTab on consume).
useEffect(() => {
if (useFederationStore.getState().pendingFriendAddPrefill) {
setActiveTab('add');
}
}, []);
const {
friends,
requests,
@@ -394,7 +406,13 @@ function AddFriendTab({
const fetchDiscoverUsers = useDiscoverStore((s) => s.fetchUsers);
const updateRelationship = useDiscoverStore((s) => s.updateRelationship);
const [query, setQuery] = useState('');
const [query, setQuery] = useState(() => {
// Consume the federation store's pending friend-add prefill at mount so
// a Retry click in the Connections panel lands here with the original
// target already in the input. The consume call clears the store value
// so subsequent mounts (e.g. tab switching) start empty.
return useFederationStore.getState().consumePendingFriendAddPrefill() ?? '';
});
const [rawSearchResults, setRawSearchResults] = useState<TaggedUser[]>([]);
const [searchLoading, setSearchLoading] = useState(false);
const [directAddLoading, setDirectAddLoading] = useState(false);