mirror of
https://github.com/desktop/desktop
synced 2024-09-19 08:02:22 +00:00
Merge pull request #12133 from desktop/thank-you-dialog
Thank you Part 1 - Thank you dialog
This commit is contained in:
commit
a8a1bb73eb
16
app/src/lib/desktop-fake-repository.ts
Normal file
16
app/src/lib/desktop-fake-repository.ts
Normal 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
|
||||||
|
)
|
|
@ -5,6 +5,7 @@ import {
|
||||||
ReleaseNote,
|
ReleaseNote,
|
||||||
ReleaseSummary,
|
ReleaseSummary,
|
||||||
} from '../models/release-notes'
|
} from '../models/release-notes'
|
||||||
|
import { encodePathAsUrl } from './path'
|
||||||
|
|
||||||
// expects a release note entry to contain a header and then some text
|
// expects a release note entry to contain a header and then some text
|
||||||
// example:
|
// example:
|
||||||
|
@ -101,3 +102,12 @@ export async function generateReleaseSummary(): Promise<ReleaseSummary> {
|
||||||
const latestRelease = releases[0]
|
const latestRelease = releases[0]
|
||||||
return getReleaseSummary(latestRelease)
|
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'
|
||||||
|
)
|
||||||
|
|
|
@ -43,4 +43,14 @@ export class Account {
|
||||||
this.name
|
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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,7 +5,7 @@ import {
|
||||||
} from './repository'
|
} from './repository'
|
||||||
import { PullRequest } from './pull-request'
|
import { PullRequest } from './pull-request'
|
||||||
import { Branch } from './branch'
|
import { Branch } from './branch'
|
||||||
import { ReleaseSummary } from './release-notes'
|
import { ReleaseNote, ReleaseSummary } from './release-notes'
|
||||||
import { IRemote } from './remote'
|
import { IRemote } from './remote'
|
||||||
import { RetryAction } from './retry-actions'
|
import { RetryAction } from './retry-actions'
|
||||||
import { WorkingDirectoryFileChange } from './status'
|
import { WorkingDirectoryFileChange } from './status'
|
||||||
|
@ -70,6 +70,7 @@ export enum PopupType {
|
||||||
CherryPick,
|
CherryPick,
|
||||||
MoveToApplicationsFolder,
|
MoveToApplicationsFolder,
|
||||||
ChangeRepositoryAlias,
|
ChangeRepositoryAlias,
|
||||||
|
ThankYou,
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Popup =
|
export type Popup =
|
||||||
|
@ -278,3 +279,9 @@ export type Popup =
|
||||||
}
|
}
|
||||||
| { type: PopupType.MoveToApplicationsFolder }
|
| { type: PopupType.MoveToApplicationsFolder }
|
||||||
| { type: PopupType.ChangeRepositoryAlias; repository: Repository }
|
| { type: PopupType.ChangeRepositoryAlias; repository: Repository }
|
||||||
|
| {
|
||||||
|
type: PopupType.ThankYou
|
||||||
|
userContributions: ReadonlyArray<ReleaseNote>
|
||||||
|
friendlyName: string
|
||||||
|
latestVersion: string | null
|
||||||
|
}
|
||||||
|
|
|
@ -135,6 +135,7 @@ import classNames from 'classnames'
|
||||||
import { dragAndDropManager } from '../lib/drag-and-drop-manager'
|
import { dragAndDropManager } from '../lib/drag-and-drop-manager'
|
||||||
import { MoveToApplicationsFolder } from './move-to-applications-folder'
|
import { MoveToApplicationsFolder } from './move-to-applications-folder'
|
||||||
import { ChangeRepositoryAlias } from './change-repository-alias/change-repository-alias-dialog'
|
import { ChangeRepositoryAlias } from './change-repository-alias/change-repository-alias-dialog'
|
||||||
|
import { ThankYou } from './thank-you'
|
||||||
|
|
||||||
const MinuteInMilliseconds = 1000 * 60
|
const MinuteInMilliseconds = 1000 * 60
|
||||||
const HourInMilliseconds = MinuteInMilliseconds * 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:
|
default:
|
||||||
return assertNever(popup, `Unknown popup type: ${popup}`)
|
return assertNever(popup, `Unknown popup type: ${popup}`)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,45 +1,17 @@
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
|
|
||||||
import { encodePathAsUrl } from '../../lib/path'
|
|
||||||
|
|
||||||
import { ReleaseNote, ReleaseSummary } from '../../models/release-notes'
|
import { ReleaseNote, ReleaseSummary } from '../../models/release-notes'
|
||||||
|
|
||||||
import { updateStore } from '../lib/update-store'
|
import { updateStore } from '../lib/update-store'
|
||||||
import { LinkButton } from '../lib/link-button'
|
import { LinkButton } from '../lib/link-button'
|
||||||
|
|
||||||
import { Dialog, DialogContent, DialogFooter } from '../dialog'
|
import { Dialog, DialogContent, DialogFooter } from '../dialog'
|
||||||
|
|
||||||
import { RichText } from '../lib/rich-text'
|
import { RichText } from '../lib/rich-text'
|
||||||
import { Repository } from '../../models/repository'
|
|
||||||
import { getDotComAPIEndpoint } from '../../lib/api'
|
|
||||||
import { shell } from '../../lib/app-shell'
|
import { shell } from '../../lib/app-shell'
|
||||||
import { ReleaseNotesUri } from '../lib/releases'
|
import { ReleaseNotesUri } from '../lib/releases'
|
||||||
import { OkCancelButtonGroup } from '../dialog/ok-cancel-button-group'
|
import { OkCancelButtonGroup } from '../dialog/ok-cancel-button-group'
|
||||||
import { GitHubRepository } from '../../models/github-repository'
|
import { DesktopFakeRepository } from '../../lib/desktop-fake-repository'
|
||||||
import { Owner } from '../../models/owner'
|
import {
|
||||||
|
ReleaseNoteHeaderLeftUri,
|
||||||
// HACK: This is needed because the `Rich`Text` component
|
ReleaseNoteHeaderRightUri,
|
||||||
// needs to know what repo to link issues against.
|
} from '../../lib/release-notes'
|
||||||
// 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'
|
|
||||||
)
|
|
||||||
|
|
||||||
interface IReleaseNotesProps {
|
interface IReleaseNotesProps {
|
||||||
readonly onDismissed: () => void
|
readonly onDismissed: () => void
|
||||||
|
@ -68,7 +40,7 @@ export class ReleaseNotes extends React.Component<IReleaseNotesProps, {}> {
|
||||||
text={entry.message}
|
text={entry.message}
|
||||||
emoji={this.props.emoji}
|
emoji={this.props.emoji}
|
||||||
renderUrlsAsLinks={true}
|
renderUrlsAsLinks={true}
|
||||||
repository={desktopRepository}
|
repository={DesktopFakeRepository}
|
||||||
/>
|
/>
|
||||||
</li>
|
</li>
|
||||||
)
|
)
|
||||||
|
|
1
app/src/ui/thank-you/index.ts
Normal file
1
app/src/ui/thank-you/index.ts
Normal file
|
@ -0,0 +1 @@
|
||||||
|
export * from './thank-you'
|
126
app/src/ui/thank-you/thank-you.tsx
Normal file
126
app/src/ui/thank-you/thank-you.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -15,6 +15,7 @@
|
||||||
@import 'dialogs/create-fork';
|
@import 'dialogs/create-fork';
|
||||||
@import 'dialogs/fork-settings';
|
@import 'dialogs/fork-settings';
|
||||||
@import 'dialogs/cherry-pick';
|
@import 'dialogs/cherry-pick';
|
||||||
|
@import 'dialogs/thank-you';
|
||||||
|
|
||||||
// The styles herein attempt to follow a flow where margins are only applied
|
// 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
|
// to the bottom of elements (with the exception of the last child). This to
|
||||||
|
|
151
app/styles/ui/dialogs/_thank-you.scss
Normal file
151
app/styles/ui/dialogs/_thank-you.scss
Normal 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) + '%');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue