Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions crates/anyps5-core/src/notifications.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ pub enum Act {
JoinInvitation(String),
/// Join the party this invitation (its ID) is for.
JoinParty(String),
/// Accept the share play invitation (its ID) from a party member.
JoinShare(String),
/// Open the friends list on its first page.
OpenFriends,
/// Open the party page.
Expand Down
101 changes: 97 additions & 4 deletions crates/anyps5-launcher/src/app/online_share.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
//! asks for another mode, changes what they may do, or ends the share, and when a command is
//! refused (docs/DESIGN.md 17, "Share play").

use anyps5_core::notifications::{Act, Kind};
use anyps5_core::online::api::ShareMode;
use anyps5_core::online::{Failure, SharePlayNotice};

Expand All @@ -11,16 +12,28 @@ use crate::app::overlay::Overlay;
use crate::screens::online_share_strings::ShareText as X;
use crate::ui::i18n::Arg;
use crate::ui::icons::Icon;
use crate::ui::sound::Cue;

/// Something about a share changed under the player.
pub fn notice(model: &mut Model, notice: SharePlayNotice) {
let language = model.language();
match notice {
SharePlayNotice::Invited(invitation) => {
// The open panel already shows the new rows.
if model.overlay != Some(Overlay::Friends) {
let text = X::ToastInvited.format(language, &[Arg::Str(&invitation.from.handle)]);
model.toast(text, Icon::GAME_CONTROLLER);
// The history keeps it either way, like a party invitation: the guest may be on
// another page, and the invitation runs out. The open panel already shows the new
// rows, so only there the toast stays away.
let text = X::ToastInvited.format(language, &[Arg::Str(&invitation.from.handle)]);
let act = Act::JoinShare(invitation.id.clone());
if model.overlay == Some(Overlay::Friends) {
model.note(Kind::Invitation, text, act);
} else {
model.announce(
Kind::Invitation,
act,
text,
Icon::GAME_CONTROLLER,
Cue::Invite,
);
}
}
SharePlayNotice::Requested(_) => {
Expand Down Expand Up @@ -66,3 +79,83 @@ pub fn failed(model: &mut Model, failure: &Failure) {
};
model.toast(text, Icon::WARNING);
}

#[cfg(test)]
mod tests {
use anyps5_core::online::api::{Account, AccountId, ShareInvitation};
use anyps5_core::online::control::Event;
use anyps5_core::online::{Line, Output};
use anyps5_core::paths::AppPaths;
use anyps5_core::settings::Preferences;

use super::*;
use crate::app::panels::notifications::primary_of;
use crate::screens::notification_strings::NotifyText;

fn invitation() -> ShareInvitation {
ShareInvitation {
id: "i1".into(),
from: Account {
id: AccountId(2),
handle: "Lumen".into(),
},
share: "s1".into(),
title_id: "T".into(),
title: "Astro Bot".into(),
mode: ShareMode::Watch,
expires: 99,
}
}

fn model_with_invitation() -> Model {
let mut model = Model::new(
Preferences::default(),
AppPaths::under(std::path::Path::new("C:/none")),
false,
);
model
.online
.apply(Line::Output(Output::Event(Event::ShareInvitation {
invitation: invitation(),
})));
model
}

#[test]
fn an_invitation_is_toasted_and_kept_in_the_history_with_an_accept() {
let mut model = model_with_invitation();
notice(&mut model, SharePlayNotice::Invited(invitation()));
assert_eq!(
model.toast.as_ref().map(|t| t.text.as_str()),
Some("Lumen invites you to share play")
);
let entry = model.notes.newest_first().next().expect("an entry");
assert_eq!(entry.kind, Kind::Invitation);
assert_eq!(entry.act, Act::JoinShare("i1".into()));
assert_eq!(
primary_of(&entry.act, &model),
Some(NotifyText::Accept),
"the entry has the primary answer while the invitation waits"
);
}

#[test]
fn with_the_friends_panel_open_only_the_history_gets_it() {
let mut model = model_with_invitation();
model.overlay = Some(Overlay::Friends);
notice(&mut model, SharePlayNotice::Invited(invitation()));
assert!(model.toast.is_none());
assert_eq!(model.notes.newest_first().count(), 1);
}

#[test]
fn an_answered_or_expired_invitation_leaves_no_button() {
let mut model = model_with_invitation();
model.online.apply(Line::Output(Output::Event(Event::Share {
can_start: false,
share: None,
})));
let act = Act::JoinShare("gone".into());
assert_eq!(primary_of(&act, &model), None);
}
}
2 changes: 1 addition & 1 deletion crates/anyps5-launcher/src/app/panels.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ mod compare;
mod friends;
mod hub;
pub(crate) mod lfg;
mod notifications;
pub(crate) mod notifications;
mod profile;

use std::collections::HashMap;
Expand Down
18 changes: 18 additions & 0 deletions crates/anyps5-launcher/src/app/panels/notifications.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

use anyps5_core::links::SettingsPage;
use anyps5_core::notifications::Act;
use anyps5_core::online::SharePlayCommand;
use anyps5_core::online::api::AccountId;

use super::PanelHost;
Expand Down Expand Up @@ -40,6 +41,12 @@ pub(crate) fn primary_of(act: &Act, model: &Model) -> Option<NT> {
.iter()
.any(|i| i.id == *id)
.then_some(NT::Join),
Act::JoinShare(id) => online
.share_play
.invitations()
.iter()
.any(|i| i.id == *id)
.then_some(NT::Accept),
Act::OpenTrophies(game) => model
.games
.iter()
Expand Down Expand Up @@ -163,6 +170,17 @@ impl PanelHost {
);
model.notes.remove(id);
}
// The same command as the Accept row on the party page.
Act::JoinShare(invitation) => {
online_action(
model,
OnlineAction::SharePlay(SharePlayCommand::Answer {
id: invitation,
accept: true,
}),
);
model.notes.remove(id);
}
Act::OpenFriends => self.open_friends_on(model, None),
Act::OpenParty => self.open_friends_on(model, Some(Page::Party)),
Act::OpenMessages(None) => self.open_friends_on(model, Some(Page::Messages)),
Expand Down
51 changes: 49 additions & 2 deletions crates/anyps5-launcher/src/app/preview.rs
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,10 @@ pub enum Scene {
/// The Notification Center with a message longer than two lines: the sentence is cut and the
/// chip reads it whole once the focus has rested on the entry.
NotificationsLong,
/// The Notification Center with a share play invitation on top of the older entries.
NotificationsShare,
/// An empty library with a share play invitation waiting: the bell carries the badge.
HomeEmptyShare,
/// Signed in, the session just ended: the question "How did it run?" below the facts.
HomeAsk,
/// Signed in, enough players reported: the Runs fact.
Expand Down Expand Up @@ -281,6 +285,8 @@ impl Scene {
"notifications" => Scene::Notifications,
"notifications-empty" => Scene::NotificationsEmpty,
"notifications-long" => Scene::NotificationsLong,
"notifications-share" => Scene::NotificationsShare,
"home-empty-share" => Scene::HomeEmptyShare,
"home-ask" => Scene::HomeAsk,
"home-runs" => Scene::HomeRuns,
"home-friends-playing" => Scene::HomeFriendsPlaying,
Expand Down Expand Up @@ -315,7 +321,7 @@ impl Scene {
})
}

pub const ALL_NAMES: [&'static str; 67] = [
pub const ALL_NAMES: [&'static str; 69] = [
"onboarding-language",
"onboarding-look",
"onboarding-look-access",
Expand Down Expand Up @@ -362,6 +368,8 @@ impl Scene {
"notifications",
"notifications-empty",
"notifications-long",
"notifications-share",
"home-empty-share",
"home-ask",
"home-runs",
"home-friends-playing",
Expand Down Expand Up @@ -829,7 +837,7 @@ pub fn apply_scene(m: &mut crate::app::model::Model, scene: Scene, games: Vec<Ga

let with_games = !matches!(
scene,
Scene::HomeEmpty | Scene::HomeScanning | Scene::HomeNoFolder
Scene::HomeEmpty | Scene::HomeEmptyShare | Scene::HomeScanning | Scene::HomeNoFolder
) && !matches!(scene, Scene::Onboarding(ob) if ob.no_games());
if matches!(scene, Scene::HomeNoFolder) {
m.edit_prefs(|p| p.games_folder = None);
Expand All @@ -839,6 +847,9 @@ pub fn apply_scene(m: &mut crate::app::model::Model, scene: Scene, games: Vec<Ga
Scene::HomeScanning | Scene::Onboarding(Ob::GamesScanning)
);
if !with_games {
if matches!(scene, Scene::HomeEmptyShare) {
share_invitation_sample(m);
}
return;
}

Expand Down Expand Up @@ -1100,6 +1111,11 @@ pub fn apply_scene(m: &mut crate::app::model::Model, scene: Scene, games: Vec<Ga
);
m.switch_overlay(crate::app::overlay::Overlay::Notifications);
}
Scene::NotificationsShare => {
notify_sample(m, &ids[at(1)], 2);
share_invitation_sample(m);
m.switch_overlay(crate::app::overlay::Overlay::Notifications);
}
Scene::HomeMessages => {
use anyps5_core::online::api::{
Account, AccountId, Conversation, ConversationKind, Conversations,
Expand Down Expand Up @@ -1268,6 +1284,37 @@ pub fn apply_scene(m: &mut crate::app::model::Model, scene: Scene, games: Vec<Ga
/// Signs the preview in as Mira, the cloud answering.
/// The newest `count` of six made-up notifications, each with something to do: a request that
/// waits, a Play Together invitation, a message, a trophy, an update and a missed call.
/// Lumen invites the player to share play: the invitation waits and its entry is unread.
fn share_invitation_sample(m: &mut crate::app::model::Model) {
use anyps5_core::notifications::{Act, Kind};
use anyps5_core::online::api::{Account, AccountId, ShareInvitation, ShareMode};
use anyps5_core::online::control::Event;
use anyps5_core::online::{Line, Output};
sign_in(m);
m.online
.apply(Line::Output(Output::Event(Event::ShareInvitation {
invitation: ShareInvitation {
id: "share-1".into(),
from: Account {
id: AccountId(4),
handle: "Lumen".into(),
},
share: "s1".into(),
title_id: "PPSA00001".into(),
title: "Astro Bot".into(),
mode: ShareMode::Watch,
expires: 0,
},
})));
let now = u64::try_from(crate::app::clock::unix_seconds()).unwrap_or(0);
m.notes.push(
Kind::Invitation,
"Lumen invites you to share play".to_owned(),
Act::JoinShare("share-1".into()),
now,
);
}

fn notify_sample(m: &mut crate::app::model::Model, game: &GameId, count: usize) {
use anyps5_core::notifications::{Act, Kind};
use anyps5_core::online::api::{Account, AccountId, Invitation, Request, SessionKind};
Expand Down
6 changes: 2 additions & 4 deletions crates/anyps5-launcher/src/screens/online/share_play.rs
Original file line number Diff line number Diff line change
Expand Up @@ -357,14 +357,12 @@ pub(super) fn rows(data: &OnlineData<'_>, rows: &mut Vec<Row>) {
rows.extend(group);
}

/// The line on Home: only while there is something to say, and only in a party.
/// The line on Home: only while there is something to say. An invitation shows even before
/// the player is in a party: it must be seen.
pub(super) fn home_row(data: &OnlineData<'_>, rows: &mut Vec<Row>) {
let online = data.online;
let play = &online.share_play;
let language = data.language;
if online.social.party.is_none() {
return;
}
let detail = if let Some(share) = play.share() {
let name = share.peer.handle.as_str();
match share.role {
Expand Down
14 changes: 14 additions & 0 deletions crates/anyps5-launcher/src/screens/online/share_play/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -321,3 +321,17 @@ fn a_stalled_picture_is_said_in_words() {
Some("The picture has stopped")
);
}

#[test]
fn an_invitation_shows_on_home_even_before_the_player_is_in_a_party() {
let mut online = online();
online.social.apply_party(None);
invite(&mut online, "i1", ShareMode::Watch);
let mut rows = Vec::new();
home_row(&data(&online), &mut rows);
assert_eq!(ids(&rows), ["sharePlay"]);
assert_eq!(
rows[0].detail.as_deref(),
Some("Lumen invites you to watch Astro Bot")
);
}
1 change: 1 addition & 0 deletions docs/DESIGN.md
Original file line number Diff line number Diff line change
Expand Up @@ -3412,6 +3412,7 @@ Gallery scenes: `friends-screen-ready`, `friends-screen-hosting`, `friends-scree
- *The host.* While the invitation waits: "Waiting for Lumen" (value "Invitation sent") and Cancel Invitation. Then "Lumen sees your game" (value Connecting… / Live / The picture has stopped, detail the game and the path), the Mode row (Watch, Take over, Together; one choice row, the note under it says what the mode means, and that Select and Start held for a second take the pad back), and Stop Sharing. A guest's request shows as a sentence ("Lumen would like to play with your pad", "Choose a mode below to agree.") and changes nothing.
- *The guest.* An invitation is a sentence ("Lumen invites you to play Astro Bot with their pad") with Accept (the first row, the primary action) and Decline. In a share: "Lumen shares Astro Bot" (phase, path; the picture opens in its own window), "You" with what the mode lets them do, Ask to Take Over and Ask to Play Together for the modes they do not have ("The host decides."), and Leave.
- *Toasts.* An invitation, a request, a change of what the guest may do, and the end, each one plain sentence; a refused command toasts one plain sentence (`no_game`, `not_in_party`, `busy`, `no_encoder`, `no_decoder`, `not_found`, `not_allowed`). Strings are in English, Turkish and German; Spanish, French and Portuguese follow with the languages pull request.
- *Seen from anywhere (0.3.9).* A share invitation expires, and the guest is not always on the party page (a real run had them on Notifications and on an empty Home). So an invitation does what a party invitation does: the toast shows on every page but the open Friends panel, and the history gets the entry either way ("Lumen invites you to share play", kind Invitation, `Act::JoinShare`) with Accept as its button and Dismiss. Accept runs the same `SharePlayCommand::Answer` as the Accept row on the party page and takes the entry with it; once the invitation is answered or has run out the entry has no button. Declining stays on the party page, as it does for party invitations. An unread entry puts the badge on the bell, so an empty library ("No games yet") shows the invitation too; the Friends Home row "Share Play" no longer waits for a party when an invitation is the reason for it. No new strings. Scenes: `notifications-share`, `home-empty-share`.
- *Not here.* The picture, the codec and the numbers stay with the worker's window. Stream and input delay are not reported by the net service yet.

Gallery scenes: `friends-share-ready`, `friends-share-waiting`, `friends-share-hosting`, `friends-share-invited`, `friends-share-guest`, `friends-share-home`.
Expand Down
Loading