mirror of
https://github.com/desktop/desktop
synced 2024-09-18 07:32:01 +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,
|
||||
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'
|
||||
)
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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}`)
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
|
|
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/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
|
||||
|
|
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