diff --git a/public/locales/en/components.json b/public/locales/en/components.json index e15fa27de..c74f0c6a6 100644 --- a/public/locales/en/components.json +++ b/public/locales/en/components.json @@ -14,5 +14,9 @@ "seconds": "seconds", "keepChanges": "Keep Changes", "cancel": "Cancel" + }, + "releaseNotesDialog": { + "title": "Release Notes", + "close": "Got It" } } \ No newline at end of file diff --git a/src-tauri/src/app_config.rs b/src-tauri/src/app_config.rs index f33242f78..60082ee21 100644 --- a/src-tauri/src/app_config.rs +++ b/src-tauri/src/app_config.rs @@ -36,6 +36,7 @@ use tari_common::configuration::Network; use tokio::fs; const LOG_TARGET: &str = "tari::universe::app_config"; +const UNIVERSE_VERSION: &str = env!("CARGO_PKG_VERSION"); #[derive(Debug, Clone, Serialize, Deserialize)] #[allow(clippy::struct_excessive_bools)] @@ -112,6 +113,8 @@ pub struct AppConfigFromFile { p2pool_stats_server_port: Option, #[serde(default = "default_false")] pre_release: bool, + #[serde(default = "default_changelog_version")] + last_changelog_version: String, #[serde(default)] airdrop_tokens: Option, } @@ -156,6 +159,7 @@ impl Default for AppConfigFromFile { show_experimental_settings: false, p2pool_stats_server_port: default_p2pool_stats_server_port(), pre_release: false, + last_changelog_version: default_changelog_version(), airdrop_tokens: None, } } @@ -275,6 +279,7 @@ pub(crate) struct AppConfig { show_experimental_settings: bool, p2pool_stats_server_port: Option, pre_release: bool, + last_changelog_version: String, airdrop_tokens: Option, } @@ -320,6 +325,7 @@ impl AppConfig { keyring_accessed: false, p2pool_stats_server_port: default_p2pool_stats_server_port(), pre_release: false, + last_changelog_version: default_changelog_version(), airdrop_tokens: None, } } @@ -398,6 +404,7 @@ impl AppConfig { self.show_experimental_settings = config.show_experimental_settings; self.p2pool_stats_server_port = config.p2pool_stats_server_port; self.pre_release = config.pre_release; + self.last_changelog_version = config.last_changelog_version; self.airdrop_tokens = config.airdrop_tokens; KEYRING_ACCESSED.store( @@ -478,6 +485,10 @@ impl AppConfig { &self.anon_id } + pub fn last_changelog_version(&self) -> &str { + &self.last_changelog_version + } + pub async fn set_mode( &mut self, mode: String, @@ -764,6 +775,15 @@ impl AppConfig { Ok(()) } + pub async fn set_last_changelog_version( + &mut self, + version: String, + ) -> Result<(), anyhow::Error> { + self.last_changelog_version = version; + self.update_config_file().await?; + Ok(()) + } + // Allow needless update because in future there may be fields that are // missing #[allow(clippy::needless_update)] @@ -811,6 +831,7 @@ impl AppConfig { show_experimental_settings: self.show_experimental_settings, p2pool_stats_server_port: self.p2pool_stats_server_port, pre_release: self.pre_release, + last_changelog_version: self.last_changelog_version.clone(), airdrop_tokens: self.airdrop_tokens.clone(), }; let config = serde_json::to_string(config)?; @@ -907,3 +928,7 @@ fn default_window_settings() -> Option { fn default_p2pool_stats_server_port() -> Option { None } + +fn default_changelog_version() -> String { + UNIVERSE_VERSION.clone().into() +} diff --git a/src-tauri/src/commands.rs b/src-tauri/src/commands.rs index 9ff3a40d2..fd8e15268 100644 --- a/src-tauri/src/commands.rs +++ b/src-tauri/src/commands.rs @@ -753,6 +753,18 @@ pub async fn get_coinbase_transactions( Ok(transactions) } +#[tauri::command] +pub async fn get_last_changelog_version( + state: tauri::State<'_, UniverseAppState>, +) -> Result { + let timer = Instant::now(); + let last_changelog_version = state.config.read().await.last_changelog_version().into(); + if timer.elapsed() > MAX_ACCEPTABLE_COMMAND_TIME { + warn!(target: LOG_TARGET, "get_last_changelog_version took too long: {:?}", timer.elapsed()); + } + Ok(last_changelog_version) +} + #[tauri::command] pub async fn import_seed_words( seed_words: Vec, @@ -1791,3 +1803,26 @@ pub async fn proceed_with_update( } Ok(()) } + +#[tauri::command] +pub async fn set_last_changelog_version( + version: String, + state: tauri::State<'_, UniverseAppState>, +) -> Result<(), String> { + let timer = Instant::now(); + + state + .config + .write() + .await + .set_last_changelog_version(version) + .await + .inspect_err(|e| error!("error at set_last_changelog_version{:?}", e)) + .map_err(|e| e.to_string())?; + + if timer.elapsed() > MAX_ACCEPTABLE_COMMAND_TIME { + warn!(target: LOG_TARGET, "set_last_changelog_version took too long: {:?}", timer.elapsed()); + } + + Ok(()) +} diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index e76c784e6..f6062caa7 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -1160,6 +1160,8 @@ fn main() { commands::try_update, commands::get_network, commands::sign_ws_data, + commands::get_last_changelog_version, + commands::set_last_changelog_version, commands::set_airdrop_tokens, commands::get_airdrop_tokens ]) diff --git a/src/components/AdminUI/groups/DialogsGroup.tsx b/src/components/AdminUI/groups/DialogsGroup.tsx index 0c441c422..2bf0d66a2 100644 --- a/src/components/AdminUI/groups/DialogsGroup.tsx +++ b/src/components/AdminUI/groups/DialogsGroup.tsx @@ -45,6 +45,12 @@ export function DialogsGroup() { > External Dependencies + + + + + + ); +} diff --git a/src/containers/floating/ReleaseNotesDialog/styles.ts b/src/containers/floating/ReleaseNotesDialog/styles.ts new file mode 100644 index 000000000..fef00ac5c --- /dev/null +++ b/src/containers/floating/ReleaseNotesDialog/styles.ts @@ -0,0 +1,54 @@ +import styled from 'styled-components'; + +export const Wrapper = styled.div` + width: 500px; + padding: 0 15px; +`; + +export const Title = styled.div` + color: #000; + font-size: 22px; + font-style: normal; + font-weight: 600; + line-height: 150%; + letter-spacing: -0.4px; + + border-bottom: 1px solid rgba(0, 0, 0, 0.05); + padding-bottom: 10px; + margin-bottom: 5px; +`; + +export const ButtonWrapper = styled.div` + border-top: 1px solid rgba(0, 0, 0, 0.05); + padding-top: 20px; + margin-top: 5px; +`; + +export const Button = styled.button` + border-radius: 49px; + background: #000; + box-shadow: 28px 28px 77px 0px rgba(0, 0, 0, 0.1); + + height: 51px; + width: 100%; + + color: #c9eb00; + text-align: center; + font-family: DrukWide, sans-serif; + font-size: 15px; + font-style: normal; + font-weight: 800; + line-height: 99.7%; + text-transform: uppercase; + + span { + display: block; + transition: transform 0.2s ease; + } + + &:hover { + span { + transform: scale(1.075); + } + } +`; diff --git a/src/containers/floating/Settings/SettingsModal.tsx b/src/containers/floating/Settings/SettingsModal.tsx index b91ff6c0d..31404815b 100644 --- a/src/containers/floating/Settings/SettingsModal.tsx +++ b/src/containers/floating/Settings/SettingsModal.tsx @@ -51,6 +51,9 @@ export default function SettingsModal() { setIsSettingsOpen(!isSettingsOpen); } + const sectionTitle = t(`tabs.${activeSection}`); + const title = activeSection === 'releaseNotes' ? sectionTitle : `${sectionTitle} ${t('settings')}`; + return ( @@ -58,7 +61,7 @@ export default function SettingsModal() { - {`${t(`tabs.${activeSection}`)} ${t('settings')}`} + {title} onOpenChange()}> diff --git a/src/containers/floating/Settings/sections/releaseNotes/AccordionItem/styles.ts b/src/containers/floating/Settings/sections/releaseNotes/AccordionItem/styles.ts index e7153351f..03f8433fa 100644 --- a/src/containers/floating/Settings/sections/releaseNotes/AccordionItem/styles.ts +++ b/src/containers/floating/Settings/sections/releaseNotes/AccordionItem/styles.ts @@ -12,6 +12,7 @@ export const Header = styled.div` padding: 20px 0; cursor: pointer; font-weight: 500; + gap: 10px; `; export const TextWrapper = styled.div` @@ -48,6 +49,7 @@ export const ChevronIcon = styled.svg<{ $isOpen: boolean }>` height: 22px; transform: scaleY(1); transition: transform 0.3s ease; + flex-shrink: 0; ${({ $isOpen }) => $isOpen && diff --git a/src/containers/floating/Settings/sections/releaseNotes/ReleaseNotes.tsx b/src/containers/floating/Settings/sections/releaseNotes/ReleaseNotes.tsx index 387bc79b8..f86bee2a5 100644 --- a/src/containers/floating/Settings/sections/releaseNotes/ReleaseNotes.tsx +++ b/src/containers/floating/Settings/sections/releaseNotes/ReleaseNotes.tsx @@ -16,10 +16,10 @@ import tariIcon from './tari-icon.png'; import packageInfo from '../../../../../../package.json'; import { useTranslation } from 'react-i18next'; import { invoke } from '@tauri-apps/api/core'; +import { useReleaseNotes } from './useReleaseNotes'; const appVersion = packageInfo.version; const versionString = `v${appVersion}`; -const CHANGELOG_URL = `https://cdn.jsdelivr.net/gh/tari-project/universe@main/CHANGELOG.md`; const parseMarkdownSections = (markdown: string): ReleaseSection[] => { const sections = markdown.split(/\n---\n/); @@ -44,7 +44,12 @@ interface ReleaseSection { content: string; } -export const ReleaseNotes = () => { +interface Props { + noHeader?: boolean; + showScrollBars?: boolean; +} + +export const ReleaseNotes = ({ noHeader, showScrollBars }: Props) => { const [sections, setSections] = useState([]); const [isLoading, setIsLoading] = useState(true); const [openSectionIndex, setOpenSectionIndex] = useState(0); @@ -52,15 +57,15 @@ export const ReleaseNotes = () => { const { t } = useTranslation(['common', 'settings'], { useSuspense: false }); + const { fetchReleaseNotes } = useReleaseNotes(); + useEffect(() => { const loadReleaseNotes = async () => { try { setIsLoading(true); - const response = await fetch(CHANGELOG_URL); - if (!response.ok) { - throw new Error(`HTTP error! status: ${response.status}`); - } - const text = await response.text(); + + const text = await fetchReleaseNotes(); + const parsedSections = parseMarkdownSections(text); setSections(parsedSections); } catch (err) { @@ -71,6 +76,8 @@ export const ReleaseNotes = () => { }; loadReleaseNotes(); + + // eslint-disable-next-line react-hooks/exhaustive-deps }, []); useEffect(() => { @@ -96,23 +103,25 @@ export const ReleaseNotes = () => { return ( - - - - {t('settings:tabs.releaseNotes')} - - {t('tari-universe')} - {t('testnet')} {versionString} - - - - {needsUpgrade && !isLoading && ( - - {t('settings:release-notes.upgrade-available')} - - )} - - - + {!noHeader && ( + + + + {t('settings:tabs.releaseNotes')} + + {t('tari-universe')} - {t('testnet')} {versionString} + + + + {needsUpgrade && !isLoading && ( + + {t('settings:release-notes.upgrade-available')} + + )} + + )} + + {isLoading ? ( {t('settings:release-notes.loading')} ) : ( diff --git a/src/containers/floating/Settings/sections/releaseNotes/styles.ts b/src/containers/floating/Settings/sections/releaseNotes/styles.ts index b58bd8b35..a0e2d762f 100644 --- a/src/containers/floating/Settings/sections/releaseNotes/styles.ts +++ b/src/containers/floating/Settings/sections/releaseNotes/styles.ts @@ -1,4 +1,4 @@ -import styled from 'styled-components'; +import styled, { css } from 'styled-components'; export const Wrapper = styled('div')` display: flex; @@ -41,12 +41,12 @@ export const Text = styled('div')` line-height: 116.667%; `; -export const MarkdownWrapper = styled('div')` +export const MarkdownWrapper = styled('div')<{ $showScrollBars?: boolean }>` position: relative; overflow: hidden; overflow-y: auto; height: calc(70vh - 210px); - padding: 0px 20px 60px 0; + padding: 0px 0px 60px 0; @media (min-width: 1200px) { height: calc(80vh - 210px); @@ -70,12 +70,14 @@ export const MarkdownWrapper = styled('div')` font-size: 14px; font-weight: 600; line-height: 110%; + margin: 0; margin-bottom: 15px; } ul { padding: 0; padding-left: 24px; + margin-bottom: 15px; li { color: ${({ theme }) => theme.palette.text.secondary}; @@ -90,6 +92,37 @@ export const MarkdownWrapper = styled('div')` border-top: 1px solid rgba(0, 0, 0, 0.05); margin: 25px 0; } + + p { + color: ${({ theme }) => theme.palette.text.secondary}; + font-size: 12px; + font-weight: 500; + line-height: 141.667%; + margin: 0; + margin-bottom: 15px; + } + + ${({ $showScrollBars }) => + $showScrollBars && + css` + &::-webkit-scrollbar { + display: block; + scrollbar-width: auto; + } + `} + + ${({ $showScrollBars }) => + $showScrollBars && + css` + &::-webkit-scrollbar { + display: block !important; + } + & { + scrollbar-width: auto; + scrollbar-color: rgba(0, 0, 0, 0.2) rgba(0, 0, 0, 0.1); + padding-right: 10px; + } + `} `; export const LoadingText = styled('div')` diff --git a/src/containers/floating/Settings/sections/releaseNotes/useReleaseNotes.ts b/src/containers/floating/Settings/sections/releaseNotes/useReleaseNotes.ts new file mode 100644 index 000000000..8dbb8611f --- /dev/null +++ b/src/containers/floating/Settings/sections/releaseNotes/useReleaseNotes.ts @@ -0,0 +1,54 @@ +import { useEffect } from 'react'; +import { useUIStore } from '@app/store/useUIStore'; +import packageInfo from '../../../../../../package.json'; +import { invoke } from '@tauri-apps/api/core'; + +interface UseReleaseNotesOptions { + triggerEffect?: boolean; +} + +function getLatestVersionFromChangelog(changelog: string): string | null { + const versionRegex = /v(\d+\.\d+\.\d+)/; + const match = changelog.match(versionRegex); + return match ? match[1] : null; +} + +const CHANGELOG_URL = `https://cdn.jsdelivr.net/gh/tari-project/universe@main/CHANGELOG.md`; + +export function useReleaseNotes(options: UseReleaseNotesOptions = {}) { + const { triggerEffect } = options; + const { setDialogToShow } = useUIStore(); + + const fetchReleaseNotes = async () => { + const response = await fetch(CHANGELOG_URL); + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + return await response.text(); + }; + + useEffect(() => { + if (!triggerEffect) return; + + const currentAppVersion = packageInfo.version; + + invoke('get_last_changelog_version').then((lastSavedChangelogVersion: unknown) => { + if (lastSavedChangelogVersion === currentAppVersion) return; + + fetchReleaseNotes() + .then((notes) => { + const releaseNotesVersion = getLatestVersionFromChangelog(notes); + + if (releaseNotesVersion != lastSavedChangelogVersion && currentAppVersion === releaseNotesVersion) { + setDialogToShow('releaseNotes'); + invoke('set_last_changelog_version', { version: releaseNotesVersion }); + } + }) + .catch((error) => { + console.error('Failed to fetch release notes:', error); + }); + }); + }, [triggerEffect, setDialogToShow]); + + return { fetchReleaseNotes, CHANGELOG_URL }; +} diff --git a/src/containers/main/MainView.tsx b/src/containers/main/MainView.tsx index ad162b7f8..6c2fcb0ce 100644 --- a/src/containers/main/MainView.tsx +++ b/src/containers/main/MainView.tsx @@ -2,9 +2,13 @@ import { DashboardContainer } from '@app/theme/styles.ts'; import { Dashboard } from '@app/containers/main/Dashboard'; import SideBar from '@app/containers/main/SideBar/SideBar.tsx'; import { useAppConfigStore } from '@app/store/useAppConfigStore'; +import { useReleaseNotes } from '../floating/Settings/sections/releaseNotes/useReleaseNotes'; export default function MainView() { const visualMode = useAppConfigStore((s) => s.visual_mode); + + useReleaseNotes({ triggerEffect: true }); + return ( diff --git a/src/containers/phase/Setup/Setup.tsx b/src/containers/phase/Setup/Setup.tsx index 5c87c4abb..2d7c63754 100644 --- a/src/containers/phase/Setup/Setup.tsx +++ b/src/containers/phase/Setup/Setup.tsx @@ -8,6 +8,7 @@ import AppVersion from './components/AppVersion'; export default function Setup() { useSetUp(); + return ( diff --git a/src/store/useUIStore.ts b/src/store/useUIStore.ts index e3477b7de..872653d6d 100644 --- a/src/store/useUIStore.ts +++ b/src/store/useUIStore.ts @@ -4,7 +4,7 @@ import { Theme } from '@app/theme/types.ts'; import { animationDarkBg, animationLightBg, setAnimationProperties } from '@app/visuals.ts'; import { useAppConfigStore } from './useAppConfigStore.ts'; -export const DIALOG_TYPES = ['logs', 'restart', 'autoUpdate', 'ludicrousConfirmation'] as const; +export const DIALOG_TYPES = ['logs', 'restart', 'autoUpdate', 'releaseNotes', 'ludicrousConfirmation'] as const; type DialogTypeTuple = typeof DIALOG_TYPES; export type DialogType = DialogTypeTuple[number];