1
0
mirror of https://github.com/spacebarchat/client.git synced 2024-11-21 18:02:32 +01:00

updater go brrrr

This commit is contained in:
Puyodead1 2023-12-22 00:10:02 -05:00
parent c7b7cf4cb7
commit 42c802c957
No known key found for this signature in database
GPG Key ID: A4FA4FEC0DD353FC
14 changed files with 729 additions and 86 deletions

View File

@ -25,6 +25,7 @@
"@tauri-apps/plugin-dialog": "2.0.0-alpha.3",
"@tauri-apps/plugin-log": "2.0.0-alpha.3",
"@tauri-apps/plugin-notification": "2.0.0-alpha.3",
"@tauri-apps/plugin-os": "2.0.0-alpha.6",
"@tauri-apps/plugin-process": "2.0.0-alpha.3",
"@tauri-apps/plugin-stronghold": "2.0.0-alpha.4",
"@tauri-apps/plugin-updater": "2.0.0-alpha.3",

View File

@ -68,6 +68,9 @@ dependencies:
'@tauri-apps/plugin-notification':
specifier: 2.0.0-alpha.3
version: 2.0.0-alpha.3
'@tauri-apps/plugin-os':
specifier: 2.0.0-alpha.6
version: 2.0.0-alpha.6
'@tauri-apps/plugin-process':
specifier: 2.0.0-alpha.3
version: 2.0.0-alpha.3
@ -3516,6 +3519,11 @@ packages:
engines: {node: '>= 18', npm: '>= 6.6.0', yarn: '>= 1.19.1'}
dev: false
/@tauri-apps/api@2.0.0-alpha.13:
resolution: {integrity: sha512-sGgCkFahF3OZAHoGN5Ozt9WK7wJlbVZSgWpPQKNag4nSOX1+Py6VDRTEWriiJHDiV+gg31CWHnNXRy6TFoZmdA==}
engines: {node: '>= 18', npm: '>= 6.6.0', yarn: '>= 1.19.1'}
dev: false
/@tauri-apps/cli-darwin-arm64@2.0.0-alpha.16:
resolution: {integrity: sha512-T/yu8+m4XrI1Ja5aVnsv4v5aGqIvwz1egHarMgh4LXrlMioJ60BoxDPfenaUokO6NVee212woFSmH6p4S7V8PA==}
engines: {node: '>= 10'}
@ -3659,6 +3667,12 @@ packages:
'@tauri-apps/api': 2.0.0-alpha.11
dev: false
/@tauri-apps/plugin-os@2.0.0-alpha.6:
resolution: {integrity: sha512-ld5p56TiWxnGHCysEfHLmRH5+Lz7rzDRC5H5WzofVNPiqp4ra1O5y1lNOWklE01Rm5P1c0t1PbUVdNGpeYA3GQ==}
dependencies:
'@tauri-apps/api': 2.0.0-alpha.13
dev: false
/@tauri-apps/plugin-process@2.0.0-alpha.3:
resolution: {integrity: sha512-XfDBtjW5s584/OJvpaWo3EGtB/lRYltWUmdRotEs4CMWZRRidDfIWf39BWLEXOnik7aMZOA9LVURaTU2QHBy+g==}
dependencies:

48
src-tauri/Cargo.lock generated
View File

@ -103,6 +103,7 @@ dependencies = [
"tauri",
"tauri-build",
"tauri-plugin-log",
"tauri-plugin-os",
"tauri-plugin-process",
"tauri-plugin-updater",
"url",
@ -1399,6 +1400,16 @@ dependencies = [
"version_check",
]
[[package]]
name = "gethostname"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0176e0459c2e4a1fe232f984bca6890e681076abb9934f6cea7c326f3fc47818"
dependencies = [
"libc",
"windows-targets 0.48.5",
]
[[package]]
name = "getrandom"
version = "0.1.16"
@ -2436,6 +2447,17 @@ dependencies = [
"pin-project-lite",
]
[[package]]
name = "os_info"
version = "3.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "006e42d5b888366f1880eda20371fedde764ed2213dc8496f49622fa0c99cd5e"
dependencies = [
"log",
"serde",
"winapi",
]
[[package]]
name = "overload"
version = "0.1.1"
@ -3502,6 +3524,15 @@ dependencies = [
"unicode-ident",
]
[[package]]
name = "sys-locale"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e801cf239ecd6ccd71f03d270d67dd53d13e90aab208bf4b8fe4ad957ea949b0"
dependencies = [
"libc",
]
[[package]]
name = "system-configuration"
version = "0.5.1"
@ -3734,6 +3765,23 @@ dependencies = [
"time",
]
[[package]]
name = "tauri-plugin-os"
version = "2.0.0-alpha.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c7cfaf07f8dcbfd4b2ce6156c4158d9d1419850ffe4e8146b6e890b5381e6906"
dependencies = [
"gethostname",
"log",
"os_info",
"serde",
"serde_json",
"serialize-to-javascript",
"sys-locale",
"tauri",
"thiserror",
]
[[package]]
name = "tauri-plugin-process"
version = "2.0.0-alpha.6"

View File

@ -21,6 +21,7 @@ tauri = { version = "2.0.0-alpha", features = ["devtools", "tray-icon"] }
tauri-plugin-updater = "2.0.0-alpha"
tauri-plugin-process = "2.0.0-alpha"
tauri-plugin-log = "2.0.0-alpha"
tauri-plugin-os = "2.0.0-alpha"
reqwest = { version = "0.11.22", features = ["json"] }
url = "2.4.1"
chrono = "0.4"

View File

@ -32,6 +32,7 @@ pub fn run() {
let mut app = tauri::Builder::default()
.plugin(tauri_plugin_process::init())
.plugin(tauri_plugin_os::init())
// Add logging plugin
.plugin(
tauri_plugin_log::Builder::default()
@ -39,11 +40,11 @@ pub fn run() {
.targets([
Target::new(TargetKind::Webview),
Target::new(TargetKind::LogDir {
file_name: Some("webview.log".into()),
file_name: Some("webview".into()),
})
.filter(|metadata| metadata.target() == WEBVIEW_TARGET),
Target::new(TargetKind::LogDir {
file_name: Some("rust.log".into()),
file_name: Some("rust".into()),
})
.filter(|metadata| metadata.target() != WEBVIEW_TARGET),
])
@ -90,7 +91,9 @@ pub fn run() {
.invoke_handler(tauri::generate_handler![
close_splashscreen,
updater::check_for_updates,
updater::install_update
updater::download_update,
updater::install_update,
updater::clear_update_cache
])
.build(context)
.expect("error while running tauri application");

View File

@ -33,14 +33,24 @@ pub fn check_for_updates<R: Runtime>(ignore_prereleases: bool, window: tauri::Wi
return;
}
match window.emit("CHECKING_FOR_UPDATE", Some(serde_json::json!({}))) {
Ok(_) => {}
Err(e) => {
println!("[Updater] Failed to emit update checking event: {:?}", e);
}
}
tauri::async_runtime::spawn(async move {
println!("Searching for update file on github.");
println!("[Updater] Searching for update file on github.");
// Custom configure the updater.
let github_releases_endpoint = "https://api.github.com/repos/spacebarchat/client/releases";
let github_releases_endpoint = match Url::parse(github_releases_endpoint) {
Ok(url) => url,
Err(e) => {
println!("Failed to parse url: {:?}. Failed to check for updates", e);
println!(
"[Updater] Failed to parse url: {:?}. Failed to check for updates",
e
);
return;
}
};
@ -54,7 +64,7 @@ pub fn check_for_updates<R: Runtime>(ignore_prereleases: bool, window: tauri::Wi
Ok(response) => response,
Err(e) => {
println!(
"Failed to send request: {:?}. Failed to check for updates",
"[Updater] Failed to send request: {:?}. Failed to check for updates",
e
);
return;
@ -63,7 +73,7 @@ pub fn check_for_updates<R: Runtime>(ignore_prereleases: bool, window: tauri::Wi
if response.status() != reqwest::StatusCode::OK {
println!(
"Non OK status code: {:?}. Failed to check for updates",
"[Updater] Non OK status code: {:?}. Failed to check for updates",
response.status()
);
return;
@ -72,7 +82,7 @@ pub fn check_for_updates<R: Runtime>(ignore_prereleases: bool, window: tauri::Wi
Ok(releases) => releases,
Err(e) => {
println!(
"Failed to parse response: {:?}. Failed to check for updates",
"[Updater] Failed to parse response: {:?}. Failed to check for updates",
e
);
return;
@ -81,7 +91,7 @@ pub fn check_for_updates<R: Runtime>(ignore_prereleases: bool, window: tauri::Wi
// check if there are any releases
if releases.len() == 0 {
println!("No releases found. Failed to check for updates");
println!("[Updater] No releases found. Failed to check for updates");
return;
}
@ -102,7 +112,7 @@ pub fn check_for_updates<R: Runtime>(ignore_prereleases: bool, window: tauri::Wi
let tauri_release_asset = match tauri_release_asset {
Some(tauri_release_asset) => tauri_release_asset,
None => {
println!("Failed to find latest.json asset. Failed to check for updates\n\nFound Assets are:");
println!("[Updater] Failed to find latest.json asset. Failed to check for updates\n\nFound Assets are:");
// Print a list of the assets found
for asset in latest_release.assets.iter() {
println!(" {:?}", asset.name);
@ -114,23 +124,29 @@ pub fn check_for_updates<R: Runtime>(ignore_prereleases: bool, window: tauri::Wi
let tauri_release_endpoint = match Url::parse(&tauri_release_asset.browser_download_url) {
Ok(url) => url,
Err(e) => {
println!("Failed to parse url: {:?}. Failed to check for updates", e);
println!(
"[Updater] Failed to parse url: {:?}. Failed to check for updates",
e
);
return;
}
};
let updater_builder = match handle
.updater_builder()
.version_comparator(|current_version, latest_version| {
println!("Current version: {}", current_version);
println!("Latest version: {}", latest_version.version.clone());
println!("[Updater] Current version: {}", current_version);
println!(
"[Updater] Latest version: {}",
latest_version.version.clone()
);
if latest_version.version > current_version {
println!("Latest version is greater than current version. ");
println!("[Updater] Latest version is greater than current version. ");
return true;
}
if latest_version.version < current_version {
println!("Latest version is lower than current version. ");
println!("[Updater] Latest version is lower than current version. ");
return false;
}
@ -142,7 +158,7 @@ pub fn check_for_updates<R: Runtime>(ignore_prereleases: bool, window: tauri::Wi
Ok(updater_builder) => updater_builder,
Err(e) => {
println!(
"Failed to build updater builder: {:?}. Failed to check for updates",
"[Updater] Failed to build updater builder: {:?}. Failed to check for updates",
e
);
return;
@ -153,18 +169,18 @@ pub fn check_for_updates<R: Runtime>(ignore_prereleases: bool, window: tauri::Wi
Ok(updater) => updater,
Err(e) => {
println!(
"Failed to build updater: {:?}. Failed to check for updates",
"[Updater] Failed to build updater: {:?}. Failed to check for updates",
e
);
return;
}
};
println!("Checking for updates");
println!("[Updater] Checking for updates");
let response = updater.check().await;
println!("Update check response: {:?}", response);
println!("[Updater] Update check response: {:?}", response);
match response {
Ok(Some(update)) => {
@ -174,40 +190,198 @@ pub fn check_for_updates<R: Runtime>(ignore_prereleases: bool, window: tauri::Wi
// }
UPDATE_INFO.lock().unwrap().replace(update.clone());
match window.emit(
"UPDATE_AVAILABLE",
Some(UpdateAvailable {
version: update.version,
body: update.body,
}),
) {
let package_path = handle
.path()
.app_local_data_dir()
.unwrap()
.join("update.sbcup");
// if we already have an update package, emit the update downloaded event
if package_path.exists() {
match window.emit(
"UPDATE_DOWNLOADED",
Some(UpdateAvailable {
version: update.version.clone(),
body: update.body.clone(),
}),
) {
Ok(_) => {}
Err(e) => {
println!("[Updater] Failed to emit update downloaded event: {:?}", e);
}
}
return;
}
// otherwise emit the update available event
match window.emit("UPDATE_AVAILABLE", Some({})) {
Ok(_) => {}
Err(e) => {
println!("Failed to emit update available event: {:?}", e);
println!("[Updater] Failed to emit update available event: {:?}", e);
}
}
return download_update(window).await;
}
Ok(None) => {
println!("[Updater] No update available");
match window.emit("UPDATE_NOT_AVAILABLE", Some({})) {
Ok(_) => {}
Err(e) => {
println!(
"[Updater] Failed to emit update not available event: {:?}",
e
);
}
}
}
_ => {}
Err(e) => {
println!("[Updater] Failed to check for updates: {:?}.", e);
}
}
});
}
#[tauri::command]
pub async fn install_update<R: Runtime>(_window: tauri::Window<R>) {
println!("Downloading and installing update!");
pub async fn download_update<R: Runtime>(window: tauri::Window<R>) {
println!("[Updater] Downloading update package");
let update = match UPDATE_INFO.lock().unwrap().clone() {
Some(update) => update,
None => {
println!("No update found to install");
println!("[Updater] No update found to download");
return;
}
};
let install_response = update.download_and_install(|_, _| {}, || {}).await;
if let Err(e) = install_response {
println!("Failed to install update: {:?}", e);
// emit UPDATE_DOWNLOADING
match window.emit("UPDATE_DOWNLOADING", Some({})) {
Ok(_) => {}
Err(e) => {
println!("[Updater] Failed to emit update downloading event: {:?}", e);
}
}
let on_chunk = |size: usize, progress: Option<u64>| {
println!(
"[Updater] Received chunk: size={}, progress={:?}",
size, progress
);
};
let on_download_finish = || {
println!("[Updater] Download finished!");
};
let download_response = update.download(on_chunk, on_download_finish).await;
if let Err(e) = download_response {
println!("[Updater] Failed to download update: {:?}", e);
} else {
println!("Update installed");
println!("[Updater] Update downloaded");
let handle = window.app_handle().clone();
let package_path = handle
.path()
.app_local_data_dir()
.unwrap()
.join("update.sbcup");
println!("[Updater] Saving update package to {:?}", package_path);
// store download_response bytes to a file
match std::fs::write(package_path.clone(), download_response.unwrap()) {
Ok(_) => {}
Err(e) => {
println!("[Updater] Failed to save update package: {:?}", e);
}
}
}
match window.emit(
"UPDATE_DOWNLOADED",
Some(UpdateAvailable {
version: update.version.clone(),
body: update.body.clone(),
}),
) {
Ok(_) => {}
Err(e) => {
println!("[Updater] Failed to emit update downloaded event: {:?}", e);
}
}
}
#[tauri::command]
pub async fn install_update<R: Runtime>(window: tauri::Window<R>) {
println!("[Updater] Installing update package");
let update = match UPDATE_INFO.lock().unwrap().clone() {
Some(update) => update,
None => {
println!("[Updater] No update found to install");
return;
}
};
let handle = window.app_handle().clone();
let package_path = handle
.path()
.app_local_data_dir()
.unwrap()
.join("update.sbcup");
// check if the update package exists
if !package_path.exists() {
println!("[Updater] No pending update found to install");
return;
}
// read in the update package bytes
let bytes = match std::fs::read(package_path.clone()) {
Ok(bytes) => bytes,
Err(e) => {
println!("[Updater] Failed to read update package: {:?}", e);
return;
}
};
let install_response = update.install(bytes);
if let Err(e) = install_response {
println!("[Updater] Failed to install update: {:?}", e);
} else {
println!("[Updater] Update installed");
// remove the update package
match std::fs::remove_file(package_path) {
Ok(_) => {}
Err(e) => {
println!("[Updater] Failed to remove update package: {:?}", e);
}
}
}
}
#[tauri::command]
pub fn clear_update_cache<R: Runtime>(window: tauri::Window<R>) {
let handle = window.app_handle().clone();
let package_path = handle
.path()
.app_local_data_dir()
.unwrap()
.join("update.sbcup");
// check if the update package exists
if !package_path.exists() {
println!("[Updater] No pending update found to clear");
return;
}
// remove the update package
match std::fs::remove_file(package_path) {
Ok(_) => {}
Err(e) => {
println!("[Updater] Failed to remove update package: {:?}", e);
}
}
}

View File

@ -52,7 +52,7 @@ function App() {
);
Globals.load();
app.loadToken();
app.loadSettings();
logger.debug("Loading complete");
app.setAppLoading(false);

View File

@ -1,4 +1,5 @@
import styled from "styled-components";
import { modalController } from "../controllers/modals";
import { useAppStore } from "../stores/AppStore";
import User from "../stores/objects/User";
import Avatar from "./Avatar";
@ -70,7 +71,11 @@ const ActionsWrapper = styled.div`
function UserPanel() {
const app = useAppStore();
const openSettingsModal = () => {};
const openSettingsModal = () => {
modalController.push({
type: "settings",
});
};
return (
<Floating

View File

@ -14,17 +14,14 @@ const IconButton = styled.button`
padding: 0;
background-color: inherit;
border: none;
&:hover {
color: red;
}
`;
const CustomIcon = styled(Icon)<{ $active?: boolean }>`
color: ${(props) => (props.$active ? "#ffffff" : "var(--text-secondary)")};
color: ${(props) => (props.$active ? "var(--text)" : "var(--text-secondary)")};
&:hover {
color: var(--text);
color: ${(props) => (props.$active ? "var(--text-secondary)" : "var(--text)")};
cursor: pointer;
}
`;
@ -115,9 +112,11 @@ interface ActionItemProps {
ariaLabel?: string;
tooltip: string;
onClick?: () => void;
disabled?: boolean;
color?: string;
}
function ActionItem({ icon, active, ariaLabel, tooltip, onClick }: ActionItemProps) {
function ActionItem({ icon, active, ariaLabel, tooltip, disabled, color, onClick }: ActionItemProps) {
const logger = useLogger("ChatHeader.tsx:ActionItem");
return (
@ -131,7 +130,13 @@ function ActionItem({ icon, active, ariaLabel, tooltip, onClick }: ActionItemPro
<FloatingTrigger>
<IconWrapper>
<IconButton onClick={onClick}>
<CustomIcon $active={active} icon={icon} size="24px" aria-label={ariaLabel} />
<CustomIcon
$active={!disabled && active}
icon={icon}
size="24px"
aria-label={ariaLabel}
color={color}
/>
</IconButton>
</IconWrapper>
</FloatingTrigger>
@ -143,7 +148,7 @@ function ActionItem({ icon, active, ariaLabel, tooltip, onClick }: ActionItemPro
* Top header for channel messages section
*/
function ChatHeader({ channel }: Props) {
const { memberListVisible, toggleMemberList } = useAppStore();
const { memberListVisible, toggleMemberList, updaterStore } = useAppStore();
return (
<Container>
@ -153,6 +158,36 @@ function ChatHeader({ channel }: Props) {
<ChannelTopic channel={channel} />
{/* Action Items */}
<ActionItemsWrapper>
{updaterStore?.checkingForUpdates && (
<ActionItem
icon="mdiCloudSync"
tooltip="Checking for Updates"
ariaLabel="Checking for Updates"
disabled
/>
)}
{updaterStore?.updateAvailable && (
<ActionItem icon="mdiUpdate" tooltip="Update Available" ariaLabel="Upate Available" disabled />
)}
{updaterStore?.updateDownloading && (
<ActionItem
icon="mdiCloudDownload"
tooltip="Downloading Update"
ariaLabel="Downloading Update"
disabled
/>
)}
{updaterStore?.updateDownloaded && (
<ActionItem
icon="mdiDownload"
tooltip="Update Ready!"
ariaLabel="Update Ready!"
color="var(--success)"
onClick={() => {
updaterStore.quitAndInstall();
}}
/>
)}
{/* <ActionItem icon="mdiPound" ariaLabel="Threads" /> */}
<DummySearch>
<span>Search</span>

View File

@ -1,3 +1,143 @@
export function SettingsModal() {
return null;
import { FormControlLabel, FormGroup, Switch } from "@mui/material";
import { getTauriVersion, getVersion } from "@tauri-apps/api/app";
import { arch, locale, platform, version } from "@tauri-apps/plugin-os";
import { observer } from "mobx-react-lite";
import React from "react";
import styled from "styled-components";
import { ModalProps } from "../../controllers/modals";
import { useAppStore } from "../../stores/AppStore";
import { isTauri } from "../../utils/Utils";
import { GIT_BRANCH, GIT_REVISION, REPO_URL } from "../../utils/revison";
import Button from "../Button";
import Link from "../Link";
import { Modal } from "./ModalComponents";
const Wrapper = styled.div`
padding: 16px 0;
gap: 8px;
display: flex;
flex-direction: column;
`;
const ActionWrapper = styled.div`
margin-top: 20px;
gap: 8px;
display: flex;
`;
const VersionWrapper = styled.div`
display: flex;
flex-direction: column;
user-select: text;
& > span {
color: var(--text-secondary);
}
`;
interface VersionInfo {
tauri: string;
app: string;
platform: {
name: string;
arch: string;
version: string;
locale: string | null;
};
}
export const SettingsModal = observer(({ ...props }: ModalProps<"settings">) => {
const app = useAppStore();
const [versionInfo, setVersionInfo] = React.useState<VersionInfo | undefined>(undefined);
const getVersionInfo = React.useMemo(
() => async () => {
const [tauriVersion, appVersion, platformName, platformArch, platformVersion, platformLocale] =
await Promise.all([getTauriVersion(), getVersion(), platform(), arch(), version(), locale()]);
setVersionInfo({
tauri: tauriVersion,
app: appVersion,
platform: {
name: platformName,
arch: platformArch,
version: platformVersion,
locale: platformLocale,
},
});
},
[],
);
React.useEffect(() => {
if (isTauri) {
getVersionInfo();
}
}, [getVersionInfo]);
return (
<Modal {...props}>
<Wrapper>
<FormGroup>
<FormControlLabel
control={<Switch checked={app.fpsShown} onChange={(e) => app.setFpsShown(e.target.checked)} />}
label="Show FPS Graph"
/>
</FormGroup>
{isTauri && (
<FormGroup>
<FormControlLabel
control={
<Switch
checked={app.updaterStore?.enabled}
onChange={(e) => app.updaterStore?.setEnabled(e.target.checked)}
/>
}
label="Enabled auto updater"
/>
</FormGroup>
)}
<VersionWrapper>
<span>
Client Version:{" "}
<Link href={`${REPO_URL}/commit/${GIT_REVISION}`} target="_blank" rel="noreferrer">
{GIT_REVISION.substring(0, 7)}
</Link>
{` `}
<Link
href={GIT_BRANCH !== "DETACHED" ? `${REPO_URL}/tree/${GIT_BRANCH}` : undefined}
target="_blank"
rel="noreferrer"
>
({GIT_BRANCH})
</Link>
</span>
{isTauri && (
<>
<span>App Version: {versionInfo?.app ?? "Fetching version information..."}</span>
<span>Tauri Version: {versionInfo?.tauri ?? "Fetching version information..."}</span>
<span>Platform: {versionInfo?.platform.name}</span>
<span>Arch: {versionInfo?.platform.arch}</span>
<span>OS Version: {versionInfo?.platform.version}</span>
<span>Locale: {versionInfo?.platform.locale ?? "Unknown"}</span>
</>
)}
</VersionWrapper>
<ActionWrapper>
<Button
palette="danger"
onClick={() => {
app.logout();
}}
>
Logout
</Button>
</ActionWrapper>
</Wrapper>
</Modal>
);
});

View File

@ -12,6 +12,7 @@ import {
JoinServerModal,
KickMemberModal,
LeaveServerModal,
SettingsModal,
} from "../../components/modals";
import { Modal } from "./types";
@ -184,4 +185,5 @@ export const modalController = new ModalControllerExtended({
// report_success: ReportSuccess,
// modify_displayname: ModifyDisplayname,
// changelog_usernames: ChangelogUsernames,
settings: SettingsModal,
});

View File

@ -10,7 +10,7 @@ export type Modal = {
key?: string;
} & (
| {
type: "add_server" | "create_server" | "join_server";
type: "add_server" | "create_server" | "join_server" | "settings";
}
| {
type: "error";

View File

@ -3,6 +3,7 @@ import { action, computed, makeAutoObservable, observable } from "mobx";
import secureLocalStorage from "react-secure-storage";
import Logger from "../utils/Logger";
import REST from "../utils/REST";
import { isTauri } from "../utils/Utils";
import AccountStore from "./AccountStore";
import ChannelStore from "./ChannelStore";
import ExperimentsStore from "./ExperimentsStore";
@ -13,6 +14,7 @@ import PresenceStore from "./PresenceStore";
import PrivateChannelStore from "./PrivateChannelStore";
import RoleStore from "./RoleStore";
import ThemeStore from "./ThemeStore";
import UpdaterStore from "./UpdaterStore";
import UserStore from "./UserStore";
import Channel from "./objects/Channel";
import Guild from "./objects/Guild";
@ -46,6 +48,8 @@ export default class AppStore {
@observable experiments = new ExperimentsStore();
@observable presences = new PresenceStore(this);
@observable queue = new MessageQueue(this);
@observable updaterStore: UpdaterStore | null = null;
@observable activeGuild: Guild | null = null;
@observable activeGuildId: Snowflake | undefined | "@me" = "@me";
@observable activeChannel: Channel | null = null;
@ -55,6 +59,10 @@ export default class AppStore {
constructor() {
makeAutoObservable(this);
if (isTauri) {
this.updaterStore = new UpdaterStore(this);
}
// bind this in toggleMemberList
this.toggleMemberList = this.toggleMemberList.bind(this);
// bind this in windowToggleFps
@ -77,43 +85,11 @@ export default class AppStore {
this.isAppLoading = value;
}
@action
setToken(token: string, save = false) {
this.token = token;
this.tokenLoaded = true;
if (save) {
secureLocalStorage.setItem("token", token);
this.logger.info("Token saved to storage");
}
}
@action
setUser(user: APIUser) {
this.account = new AccountStore(user);
}
@action
loadToken() {
const token = secureLocalStorage.getItem("token") as string | null;
this.tokenLoaded = true;
if (token) {
this.logger.debug("Loaded token from storage.");
this.setToken(token);
} else {
this.logger.debug("No token found in storage.");
this.setGatewayReady(true);
}
}
@action
logout() {
this.token = null;
this.tokenLoaded = false;
secureLocalStorage.removeItem("token");
}
@action
setNetworkConnected(value: boolean) {
this.isNetworkConnected = value;
@ -143,11 +119,6 @@ export default class AppStore {
this.activeChannel = (id ? this.channels.get(id) : null) ?? null;
}
@action
setFpsShown(value: boolean) {
this.fpsShown = value;
}
@action
toggleMemberList() {
this.memberListVisible = !this.memberListVisible;
@ -157,6 +128,73 @@ export default class AppStore {
windowToggleFps() {
this.setFpsShown(!this.fpsShown);
}
// stuff mainly for settings, really anything that uses local storage
@action
setToken(token: string, save = false) {
this.token = token;
this.tokenLoaded = true;
if (save) {
secureLocalStorage.setItem("token", token);
this.logger.info("Token saved to storage");
}
}
@action
loadToken() {
const token = secureLocalStorage.getItem("token") as string | null;
this.tokenLoaded = true;
if (token) {
this.logger.debug("Loaded token from storage.");
this.setToken(token);
} else {
this.logger.debug("No token found in storage.");
this.setGatewayReady(true);
}
}
@action
logout() {
this.token = null;
this.tokenLoaded = false;
this.isAppLoading = false;
this.isGatewayReady = true;
secureLocalStorage.clear();
}
@action
setFpsShown(value: boolean) {
this.fpsShown = value;
secureLocalStorage.setItem("fpsShown", value);
}
@action
loadFpsShown() {
this.fpsShown = (secureLocalStorage.getItem("fpsShown") as boolean | null) ?? false;
}
@action
setUpdaterEnabled(value: boolean) {
this.updaterStore?.setEnabled(value);
secureLocalStorage.setItem("updaterEnabled", value);
}
@action
loadUpdaterEnabled() {
this.updaterStore?.setEnabled((secureLocalStorage.getItem("updaterEnabled") as boolean | null) ?? true);
}
@action
loadSettings() {
this.loadFpsShown();
this.loadToken();
this.loadUpdaterEnabled();
}
}
export const appStore = new AppStore();

182
src/stores/UpdaterStore.ts Normal file
View File

@ -0,0 +1,182 @@
import { listen } from "@tauri-apps/api/event";
import { invoke } from "@tauri-apps/api/primitives";
import { action, makeAutoObservable, observable } from "mobx";
import useLogger from "../hooks/useLogger";
import Logger from "../utils/Logger";
import AppStore from "./AppStore";
export default class UpdaterStore {
private readonly logger: Logger = useLogger("UpdaterStore");
@observable initialized: boolean = false;
@observable enabled: boolean = true;
@observable checkingForUpdates: boolean = false;
@observable updateDownloading: boolean = false;
@observable updateDownloaded: boolean = false;
@observable updateAvailable: boolean = false;
@observable timer: NodeJS.Timeout | null = null;
constructor(private readonly app: AppStore) {
this.logger.info("Initializing UpdaterStore");
makeAutoObservable(this);
const setupListeners = async () => {
await listen("CHECKING_FOR_UPDATE", () => {
this.logger.debug("Checking for updates");
this.setCheckingForUpdates(true);
this.setUpdateAvailable(false);
this.setUpdateDownloading(false);
this.setUpdateDownloaded(false);
});
await listen("UPDATE_AVAILABLE", () => {
this.logger.debug("Update available");
this.setCheckingForUpdates(false);
this.setUpdateAvailable(true);
this.setUpdateDownloading(false);
this.setUpdateDownloaded(false);
});
await listen("UPDATE_NOT_AVAILABLE", () => {
this.logger.debug("Update not available");
this.setCheckingForUpdates(false);
this.setUpdateAvailable(false);
this.setUpdateDownloading(false);
this.setUpdateDownloaded(false);
});
await listen("UPDATE_DOWNLOADING", () => {
this.logger.debug("Update downloading");
this.setCheckingForUpdates(false);
this.setUpdateAvailable(false);
this.setUpdateDownloading(true);
this.setUpdateDownloaded(false);
});
await listen("UPDATE_DOWNLOADED", () => {
this.logger.debug("Update downloaded");
this.setCheckingForUpdates(false);
this.setUpdateAvailable(false);
this.setUpdateDownloading(false);
this.setUpdateDownloaded(true);
});
};
setupListeners();
// @ts-expect-error - expose updater to window, don't use use this though
window.updater = {
setUpdateAvailable: this.setUpdateAvailable.bind(this),
setUpdateDownloading: this.setUpdateDownloading.bind(this),
setUpdateDownloaded: this.setUpdateDownloaded.bind(this),
setCheckingForUpdates: this.setCheckingForUpdates.bind(this),
checkForUpdates: this.checkForUpdates.bind(this),
downloadUpdate: this.downloadUpdate.bind(this),
quitAndInstall: this.quitAndInstall.bind(this),
clearUpdateCache: this.clearCache.bind(this),
};
}
@action
setCheckingForUpdates(value: boolean) {
this.checkingForUpdates = value;
}
@action
setUpdateAvailable(value: boolean) {
this.updateAvailable = value;
}
@action
setUpdateDownloading(value: boolean) {
this.updateDownloading = value;
}
@action
setUpdateDownloaded(value: boolean) {
this.updateDownloaded = value;
}
async checkForUpdates() {
if (this.checkingForUpdates) {
this.logger.warn("Already checking for updates, skipping check");
return;
}
// if (this.app.settings.ignoreUpdates) {
// this.logger.warn("Ignoring updates, skipping check");
// return;
// }
this.logger.debug("Invoking update check");
await invoke("check_for_updates", { ignorePrereleases: false });
}
async downloadUpdate() {
if (this.updateDownloading) {
this.logger.warn("Already downloading an update");
return;
}
if (this.updateDownloaded) {
this.logger.warn("An update is already pending installation");
return;
}
this.logger.debug("Invoking update download");
await invoke("download_update");
}
async quitAndInstall() {
if (!this.updateAvailable) {
this.logger.warn("No update is pending installation");
return;
}
this.logger.debug("Invoking update install");
await invoke("install_update");
}
@action
setEnabled(value: boolean) {
this.enabled = value;
if (value) {
this.enable();
} else {
this.disable();
}
}
@action
async enable() {
this.logger.debug("Enabling updater");
if (this.initialized) {
this.logger.debug("Updater already initialized, skipping init");
return;
}
// initial update check
await this.checkForUpdates();
// start an update timer
this.timer = setInterval(async () => {
this.logger.debug("[UpdateTimer] Checking for updates");
await this.checkForUpdates();
}, 36e5); // 1 hour
this.initialized = true;
}
@action
disable() {
this.logger.debug("Disabling updater");
if (this.timer) {
clearInterval(this.timer);
}
}
async clearCache() {
this.logger.debug("Clearing update cache");
await invoke("clear_update_cache");
}
}