Merge pull request #12133 from desktop/thank-you-dialog

Thank you Part 1 - Thank you dialog
This commit is contained in:
tidy-dev 2021-05-05 07:49:58 -04:00 committed by GitHub
commit a8a1bb73eb
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 341 additions and 35 deletions

View file

@ -0,0 +1,16 @@
import { Repository } from '../models/repository'
import { getDotComAPIEndpoint } from './api'
import { GitHubRepository } from '../models/github-repository'
import { Owner } from '../models/owner'
// HACK: This is needed because the `Rich`Text` component needs to know what
// repo to link issues against. Used when we can't rely on the repo info we keep
// in state because we it need Desktop specific, so we've stubbed out this repo
const desktopOwner = new Owner('desktop', getDotComAPIEndpoint(), -1)
const desktopUrl = 'https://github.com/desktop/desktop'
export const DesktopFakeRepository = new Repository(
'',
-1,
new GitHubRepository('desktop', desktopOwner, -1, false, desktopUrl),
true
)

View file

@ -5,6 +5,7 @@ import {
ReleaseNote,
ReleaseSummary,
} from '../models/release-notes'
import { encodePathAsUrl } from './path'
// expects a release note entry to contain a header and then some text
// example:
@ -101,3 +102,12 @@ export async function generateReleaseSummary(): Promise<ReleaseSummary> {
const latestRelease = releases[0]
return getReleaseSummary(latestRelease)
}
export const ReleaseNoteHeaderLeftUri = encodePathAsUrl(
__dirname,
'static/release-note-header-left.svg'
)
export const ReleaseNoteHeaderRightUri = encodePathAsUrl(
__dirname,
'static/release-note-header-right.svg'
)

View file

@ -43,4 +43,14 @@ export class Account {
this.name
)
}
/**
* Get a name to display
*
* This will by default return the 'name' as it is the friendly name.
* However, if not defined, we return the login
*/
public get friendlyName(): string {
return this.name !== '' ? this.name : this.login
}
}

View file

@ -5,7 +5,7 @@ import {
} from './repository'
import { PullRequest } from './pull-request'
import { Branch } from './branch'
import { ReleaseSummary } from './release-notes'
import { ReleaseNote, ReleaseSummary } from './release-notes'
import { IRemote } from './remote'
import { RetryAction } from './retry-actions'
import { WorkingDirectoryFileChange } from './status'
@ -70,6 +70,7 @@ export enum PopupType {
CherryPick,
MoveToApplicationsFolder,
ChangeRepositoryAlias,
ThankYou,
}
export type Popup =
@ -278,3 +279,9 @@ export type Popup =
}
| { type: PopupType.MoveToApplicationsFolder }
| { type: PopupType.ChangeRepositoryAlias; repository: Repository }
| {
type: PopupType.ThankYou
userContributions: ReadonlyArray<ReleaseNote>
friendlyName: string
latestVersion: string | null
}

View file

@ -135,6 +135,7 @@ import classNames from 'classnames'
import { dragAndDropManager } from '../lib/drag-and-drop-manager'
import { MoveToApplicationsFolder } from './move-to-applications-folder'
import { ChangeRepositoryAlias } from './change-repository-alias/change-repository-alias-dialog'
import { ThankYou } from './thank-you'
const MinuteInMilliseconds = 1000 * 60
const HourInMilliseconds = MinuteInMilliseconds * 60
@ -2064,6 +2065,17 @@ export class App extends React.Component<IAppProps, IAppState> {
/>
)
}
case PopupType.ThankYou:
return (
<ThankYou
key="thank-you"
emoji={this.state.emoji}
userContributions={popup.userContributions}
friendlyName={popup.friendlyName}
latestVersion={popup.latestVersion}
onDismissed={onPopupDismissedFn}
/>
)
default:
return assertNever(popup, `Unknown popup type: ${popup}`)
}

View file

@ -1,45 +1,17 @@
import * as React from 'react'
import { encodePathAsUrl } from '../../lib/path'
import { ReleaseNote, ReleaseSummary } from '../../models/release-notes'
import { updateStore } from '../lib/update-store'
import { LinkButton } from '../lib/link-button'
import { Dialog, DialogContent, DialogFooter } from '../dialog'
import { RichText } from '../lib/rich-text'
import { Repository } from '../../models/repository'
import { getDotComAPIEndpoint } from '../../lib/api'
import { shell } from '../../lib/app-shell'
import { ReleaseNotesUri } from '../lib/releases'
import { OkCancelButtonGroup } from '../dialog/ok-cancel-button-group'
import { GitHubRepository } from '../../models/github-repository'
import { Owner } from '../../models/owner'
// HACK: This is needed because the `Rich`Text` component
// needs to know what repo to link issues against.
// Since release notes are Desktop specific, we can't
// rely on the repo info we keep in state, so we've
// stubbed out this repo
const desktopOwner = new Owner('desktop', getDotComAPIEndpoint(), -1)
const desktopUrl = 'https://github.com/desktop/desktop'
const desktopRepository = new Repository(
'',
-1,
new GitHubRepository('desktop', desktopOwner, -1, false, desktopUrl),
true
)
const ReleaseNoteHeaderLeftUri = encodePathAsUrl(
__dirname,
'static/release-note-header-left.svg'
)
const ReleaseNoteHeaderRightUri = encodePathAsUrl(
__dirname,
'static/release-note-header-right.svg'
)
import { DesktopFakeRepository } from '../../lib/desktop-fake-repository'
import {
ReleaseNoteHeaderLeftUri,
ReleaseNoteHeaderRightUri,
} from '../../lib/release-notes'
interface IReleaseNotesProps {
readonly onDismissed: () => void
@ -68,7 +40,7 @@ export class ReleaseNotes extends React.Component<IReleaseNotesProps, {}> {
text={entry.message}
emoji={this.props.emoji}
renderUrlsAsLinks={true}
repository={desktopRepository}
repository={DesktopFakeRepository}
/>
</li>
)

View file

@ -0,0 +1 @@
export * from './thank-you'

View file

@ -0,0 +1,126 @@
import * as React from 'react'
import { DesktopFakeRepository } from '../../lib/desktop-fake-repository'
import {
ReleaseNoteHeaderLeftUri,
ReleaseNoteHeaderRightUri,
} from '../../lib/release-notes'
import { ReleaseNote } from '../../models/release-notes'
import { Dialog, DialogContent } from '../dialog'
import { RichText } from '../lib/rich-text'
interface IThankYouProps {
readonly onDismissed: () => void
readonly emoji: Map<string, string>
readonly userContributions: ReadonlyArray<ReleaseNote>
readonly friendlyName: string
readonly latestVersion: string | null
}
export class ThankYou extends React.Component<IThankYouProps, {}> {
private renderList(
releaseEntries: ReadonlyArray<ReleaseNote>
): JSX.Element | null {
if (releaseEntries.length === 0) {
return null
}
const options = new Array<JSX.Element>()
for (const [i, entry] of releaseEntries.entries()) {
options.push(
<li key={i}>
<RichText
text={entry.message}
emoji={this.props.emoji}
renderUrlsAsLinks={true}
repository={DesktopFakeRepository}
/>
</li>
)
}
return (
<div className="section">
<ul className="entries">{options}</ul>
</div>
)
}
private renderConfetti(): JSX.Element | null {
const confetti = new Array<JSX.Element>()
const howMuchConfetti = 1500
for (let i = 0; i < howMuchConfetti; i++) {
confetti.push(<div key={i} className="confetti"></div>)
}
return <>{confetti}</>
}
public render() {
const dialogHeader = (
<div className="release-notes-header">
<div className="header-graphics">
<img
className="release-note-graphic-left"
src={ReleaseNoteHeaderLeftUri}
/>
<div className="img-space"></div>
<img
className="release-note-graphic-right"
src={ReleaseNoteHeaderRightUri}
/>
</div>
<div className="title">
<div className="thank-you">
Thank you {this.props.friendlyName}!{' '}
<RichText
text={':tada:'}
emoji={this.props.emoji}
renderUrlsAsLinks={true}
/>
</div>
<div className="thank-you-note">
The Desktop team wants to thank you personally.
</div>
</div>
</div>
)
const thankYou = 'Thank you for all your hard work on GitHub Desktop'
let thankYouNote
if (this.props.latestVersion === null) {
thankYouNote = <>{thankYou}</>
} else {
thankYouNote = (
<>
{thankYou} version {this.props.latestVersion}
</>
)
}
return (
<Dialog
id="thank-you-notes"
onDismissed={this.props.onDismissed}
title={dialogHeader}
>
<DialogContent>
<div className="container">
<div className="thank-you-note">{thankYouNote}.</div>
<div className="contributions-heading">You contributed:</div>
<div className="contributions">
{this.renderList(this.props.userContributions)}
</div>
<div
className="confetti-container"
onClick={this.props.onDismissed}
>
{this.renderConfetti()}
</div>
</div>
</DialogContent>
</Dialog>
)
}
}

View file

@ -15,6 +15,7 @@
@import 'dialogs/create-fork';
@import 'dialogs/fork-settings';
@import 'dialogs/cherry-pick';
@import 'dialogs/thank-you';
// The styles herein attempt to follow a flow where margins are only applied
// to the bottom of elements (with the exception of the last child). This to

View file

@ -0,0 +1,151 @@
@import '../../../styles/variables';
#thank-you-notes {
max-height: 450px;
.dialog-content {
// we'll own the layout inside here
padding: 0;
}
.dialog-header {
height: 100px;
position: relative;
.release-notes-header {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
padding: 0 var(--spacing-double);
}
.header-graphics {
display: flex;
.img-space {
flex-grow: 2;
display: flex;
}
}
.release-note-graphic-left {
margin-right: 20px;
}
.release-note-graphic-right {
margin-left: 20px;
}
.title {
text-align: center;
position: absolute;
width: 100%;
top: 0;
left: 0;
padding-top: 6%;
.thank-you-note {
font-size: var(--font-size-md);
font-weight: normal;
}
.thank-you {
font-size: var(--font-size-md);
font-weight: var(--font-weight-semibold);
div {
display: inline;
}
}
}
}
ul {
list-style: none;
padding-left: 0;
li {
padding-left: 0;
}
}
a.close {
align-self: flex-start;
z-index: 1;
}
.container {
width: 600px;
padding-left: 80px;
padding-right: 80px;
padding-top: var(--spacing-triple);
padding-bottom: var(--spacing-triple);
overflow-x: hidden;
overflow-y: auto;
.thank-you-note {
font-weight: var(--font-weight-semibold);
padding-bottom: var(--spacing-triple);
}
.contributions-heading {
font-weight: var(--font-weight-semibold);
}
.contributions {
max-height: 150px;
}
}
.confetti-container {
position: absolute;
width: 110vw;
height: 110vh;
top: -50vh;
left: -33vw;
animation: remove-confetti-container 0s ease-in 4s forwards;
animation-iteration-count: 1;
animation-fill-mode: forwards;
}
@keyframes remove-confetti-container {
to {
width: 0;
height: 0;
overflow: hidden;
}
}
.confetti {
position: absolute;
}
$colors: (#e7001b, #ffe600, #0ebd25, #0f2679, #e21bd2);
@for $i from 0 through 1500 {
$w: random(8);
$l: random(100);
.confetti:nth-child(#{$i}) {
width: #{$w}px;
height: #{$w * 0.4}px;
background-color: nth($colors, random(5));
top: -10%;
left: unquote($l + '%');
opacity: random() + 0.5;
transform: rotate(#{random() * 360}deg);
animation-name: falling-#{$i};
animation-duration: unquote(5 + random() + 's');
animation-delay: unquote('-' + random() + 's');
animation-iteration-count: 1;
}
@keyframes falling-#{$i} {
100% {
top: 110%;
left: unquote($l + random(15) + '%');
}
}
}
}