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
10 changes: 10 additions & 0 deletions manifest.webapp
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,16 @@
"identities": {
"description": "Required to display identities debug data",
"type": "io.cozy.identities"
},
"announcements-dev": {
"type": "cc.cozycloud.announcements.dev",
"verbs": ["GET"],
"description": "Remote-doctype required to get announcements, for development purposes"
},
"announcements-uploads-dev": {
"type": "cc.cozycloud.announcements.dev.uploads",
"verbs": ["GET"],
"description": "Remote-doctype required to get announcements images, for development purposes"
}
},
"routes": {
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@
"cozy-client": "^48.17.0",
"cozy-device-helper": "2.7.0",
"cozy-doctypes": "1.83.8",
"cozy-flags": "3.2.2",
"cozy-flags": "4.0.0",
"cozy-harvest-lib": "^29.1.0",
"cozy-intent": "^2.19.2",
"cozy-interapp": "^0.9.0",
Expand Down
44 changes: 44 additions & 0 deletions src/components/Announcements/Announcements.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import React, { FC, useState } from 'react'
import { differenceInHours } from 'date-fns'

import flag from 'cozy-flags'

import { AnnouncementsDialog } from './AnnouncementsDialog'
import { useAnnouncements } from 'hooks/useAnnouncements'
import { AnnouncementsConfigFlag } from './types'
import { useAnnouncementsSettings } from 'hooks/useAnnouncementsSettings'

const Announcements: FC = () => {
const config = flag<AnnouncementsConfigFlag>('home.announcements')
const [hasBeenDismissed, setBeenDismissed] = useState(false)
const { values, save } = useAnnouncementsSettings()

const handleDismiss = (): void => {
save({
dismissedAt: new Date().toISOString()
})
setBeenDismissed(true)
}

const moreThan = config?.delayAfterDismiss ?? 24
const hasBeenDismissedForMoreThan = values.dismissedAt
? differenceInHours(Date.parse(values.dismissedAt), new Date()) >= moreThan
: true
const canBeDisplayed = !hasBeenDismissed && hasBeenDismissedForMoreThan
const announcements = useAnnouncements({
canBeDisplayed
})

if (canBeDisplayed && announcements.length > 0) {
return (
<AnnouncementsDialog
announcements={announcements}
onDismiss={handleDismiss}
/>
)
}

return null
}

export { Announcements }
101 changes: 101 additions & 0 deletions src/components/Announcements/AnnouncementsDialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import React, { useState, FC } from 'react'
import SwipeableViews from 'react-swipeable-views'

import { FixedActionsDialog } from 'cozy-ui/transpiled/react/CozyDialogs'
import CozyTheme from 'cozy-ui/transpiled/react/providers/CozyTheme'
import MobileStepper from 'cozy-ui/transpiled/react/MobileStepper'
import IconButton from 'cozy-ui/transpiled/react/IconButton'
import Icon from 'cozy-ui/transpiled/react/Icon'
import LeftIcon from 'cozy-ui/transpiled/react/Icons/Left'
import RightIcon from 'cozy-ui/transpiled/react/Icons/Right'
import useBreakpoints from 'cozy-ui/transpiled/react/providers/Breakpoints'

import { AnnouncementsDialogContent } from './AnnouncementsDialogContent'
import { Announcement } from './types'
import { useAnnouncementsSettings } from 'hooks/useAnnouncementsSettings'

interface AnnouncementsDialogProps {
announcements: Array<Announcement>
onDismiss: () => void
}

const AnnouncementsDialog: FC<AnnouncementsDialogProps> = ({
announcements,
onDismiss
}) => {
const { values, save } = useAnnouncementsSettings()
const { isMobile } = useBreakpoints()

const [activeStep, setActiveStep] = useState(0)

const handleBack = (): void => {
setActiveStep(activeStep - 1)
}

const handleNext = (): void => {
const uuid = announcements[activeStep].attributes.uuid
if (!values?.seen.includes(uuid)) {
save({
seen: [...(values?.seen ?? []), uuid]
})
}
setActiveStep(activeStep + 1)
}

const handleChangedIndex = (index: number): void => {
setActiveStep(index)
}

const maxSteps = announcements.length

return (
<CozyTheme variant="normal">
<FixedActionsDialog
open
onClose={onDismiss}
content={
<SwipeableViews
index={activeStep}
onChangeIndex={handleChangedIndex}
animateTransitions={isMobile}
>
{announcements.map((announcement, index) => (
<AnnouncementsDialogContent
key={index}
isLast={index === maxSteps - 1}
announcement={announcement}
onDismiss={onDismiss}
onNext={handleNext}
/>
))}
</SwipeableViews>
}
actions={
maxSteps > 1 ? (
<MobileStepper
className="u-mh-auto"
steps={maxSteps}
position="static"
activeStep={activeStep}
nextButton={
<IconButton
onClick={handleNext}
disabled={activeStep === maxSteps - 1}
>
<Icon icon={RightIcon} />
</IconButton>
}
backButton={
<IconButton onClick={handleBack} disabled={activeStep === 0}>
<Icon icon={LeftIcon} />
</IconButton>
}
/>
) : null
}
/>
</CozyTheme>
)
}

export { AnnouncementsDialog }
105 changes: 105 additions & 0 deletions src/components/Announcements/AnnouncementsDialogContent.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import React, { FC } from 'react'
import { useI18n } from 'cozy-ui/transpiled/react/providers/I18n'
import Typography from 'cozy-ui/transpiled/react/Typography'
import Buttons from 'cozy-ui/transpiled/react/Buttons'

import { Announcement } from './types'
import { useAnnouncementsImage } from 'hooks/useAnnouncementsImage'

interface AnnouncementsDialogContentProps {
isLast: boolean
announcement: Announcement
onDismiss: () => void
onNext: () => void
}

const AnnouncementsDialogContent: FC<AnnouncementsDialogContentProps> = ({
isLast,
announcement,
onDismiss,
onNext
}) => {
const { t, f } = useI18n()
const primaryImage = useAnnouncementsImage(
announcement.attributes.primary_image.data.attributes.formats.small.url
)
const secondaryImage = useAnnouncementsImage(
announcement.attributes.secondary_image.data?.attributes.formats.thumbnail
.url
)

const handleMainAction = (): void => {
if (announcement.attributes.main_action?.link) {
window.open(announcement.attributes.main_action.link, '_blank')
}
}

return (
<div className="u-flex u-flex-column u-flex-items-center">
{primaryImage ? (
<img
src={primaryImage}
alt={
announcement.attributes.primary_image.data.attributes
.alternativeText
}
className="u-mt-1 u-mb-2 u-bdrs-3 u-maw-100"
style={{
objectFit: 'cover',
objectPosition: '100% 0'
}}
/>
) : null}
<Typography align="center" className="u-mb-half" variant="h3">
{announcement.attributes.title}
</Typography>
<Typography
align="center"
color="textSecondary"
className="u-mb-1"
variant="body2"
>
{f(
announcement.attributes.start_at,
t('AnnouncementsDialogContent.dateFormat')
)}
</Typography>
<Typography align="center" className="u-mb-1">
{announcement.attributes.content}
</Typography>
{announcement.attributes.main_action ? (
<Buttons
className="u-mb-half"
variant="secondary"
label={announcement.attributes.main_action.label}
onClick={handleMainAction}
/>
) : null}
<Buttons
label={t(
isLast
? 'AnnouncementsDialogContent.understand'
: 'AnnouncementsDialogContent.next'
)}
variant="secondary"
onClick={isLast ? onDismiss : onNext}
/>
{secondaryImage ? (
<img
src={secondaryImage}
alt={
announcement.attributes.secondary_image.data?.attributes
.alternativeText
}
className="u-mt-1 u-w-2 u-h-2"
style={{
objectFit: 'cover',
objectPosition: '100% 0'
}}
/>
) : null}
</div>
)
}

export { AnnouncementsDialogContent }
19 changes: 19 additions & 0 deletions src/components/Announcements/helpers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { Announcement } from './types'

export const getUnseenAnnouncements = (
data: Announcement[],
announcements_seen: string[]
): Announcement[] => {
return data.filter(announcement => {
if (announcements_seen) {
return !announcements_seen.includes(announcement.attributes.uuid)
}
return true
})
}

export const isAnnouncement = (
announcement: unknown
): announcement is Announcement => {
return (announcement as Announcement).attributes?.title !== undefined
}
46 changes: 46 additions & 0 deletions src/components/Announcements/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
export interface Announcement {
id: string
type: string
attributes: {
title: string
content: string
start_at: string
uuid: string
main_action?: {
label: string
link: string
}
primary_image: {
data: {
attributes: {
formats: {
small: {
url: string
}
}
alternativeText?: string
}
}
}
secondary_image: {
data: {
attributes: {
formats: {
thumbnail: {
url: string
}
}
alternativeText?: string
}
} | null
}
}
}

export interface AnnouncementsConfig {
remoteDoctype: string
channels: string
delayAfterDismiss: number
}

export type AnnouncementsConfigFlag = AnnouncementsConfig | null
2 changes: 2 additions & 0 deletions src/containers/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ import {
import { useI18n } from 'cozy-ui/transpiled/react/providers/I18n'
import SectionDialog from 'components/Sections/SectionDialog'
import { SentryRoutes } from 'lib/sentry'
import { Announcements } from 'components/Announcements/Announcements'

window.flag = window.flag || flag
window.minilog = minilog
Expand Down Expand Up @@ -134,6 +135,7 @@ const App = ({ accounts, konnectors, triggers }) => {
<ReloadFocus />
<MainView>
<BackupNotification />
<Announcements />
<Corner />
<div
className="u-flex u-flex-column u-flex-content-start u-flex-content-stretch u-w-100 u-m-auto u-pos-relative"
Expand Down
15 changes: 0 additions & 15 deletions src/cozy-flags.d.ts

This file was deleted.

Loading