Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add copy event id/hash button. #114

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
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
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
node_modules/
dist/
ts/**/*.js
ts/**/*.js
.vscode/
6 changes: 6 additions & 0 deletions scss/main.scss
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,12 @@ body {
color: $red;
}

.copy {
color: $dimmed_foreground;
cursor: pointer;
margin-left: 5px;
}

.description {
margin-top: 20px;
text-align: left;
Expand Down
9 changes: 2 additions & 7 deletions ts/CancelledEvent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import * as moment from 'moment';
import ComponentsArray from './ComponentsArray';
import Countdown from './Countdown';
import UiComponent from './UiComponent';
import Timestamp from './Timestamp';

export default class CancelledEvent implements UiComponent {
constructor(private _event: dto.Event) {
Expand All @@ -12,13 +13,7 @@ export default class CancelledEvent implements UiComponent {
appendTo(entry: HTMLElement | null): void {
new html.Div(
new ComponentsArray([
new html.Div(
new html.Href(
`#_${this._event.datetime.utc().unix()}`,
new html.Text(`${this._event.datetime.utc().unix()}`)
),
{"class": "timestamp"}
),
new Timestamp(this._event.timestamp()),
new html.H1(
new html.Href(
`${this._event.url}`,
Expand Down
9 changes: 2 additions & 7 deletions ts/CurrentEvent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import ComponentsArray from './ComponentsArray';
import Countdown from './Countdown';
import UiComponent from './UiComponent';
import TwitchPlayer from './TwitchPlayer';
import Timestamp from './Timestamp'

export default class CurrentEvent implements UiComponent {
constructor(private _event: dto.Event) {
Expand All @@ -13,13 +14,7 @@ export default class CurrentEvent implements UiComponent {
appendTo(entry: HTMLElement | null): void {
new html.Div(
new ComponentsArray([
new html.Div(
new html.Href(
`#_${this._event.datetime.utc().unix()}`,
new html.Text(`${this._event.datetime.utc().unix()}`)
),
{"class": "timestamp"}
),
new Timestamp(this._event.timestamp()),
new html.Div(
new html.Href(
this._event.channel ? this._event.channel : "https://twitch.tv/tsoding",
Expand Down
5 changes: 3 additions & 2 deletions ts/Event.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ export default class Event implements UiComponent {
}

appendTo(entry: HTMLElement | null): void {
let secondsDiff = moment().diff(this._event.datetime, 'seconds');
const secondsDiff = moment().diff(this._event.datetime, 'seconds');

if (this.isCancelled()) {
new CancelledEvent(this._event).appendTo(entry);
Expand All @@ -25,7 +25,8 @@ export default class Event implements UiComponent {
new FutureEvent(this._event).appendTo(entry);
}

const hashId = "#_" + this._event.datetime.utc().unix();
const hashId = "#_" + this._event.timestamp();

if (window.location.hash == hashId) {
window.location.hash = "";
setTimeout(() => { window.location.hash = hashId; }, 0);
Expand Down
4 changes: 2 additions & 2 deletions ts/EventsForDay.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,8 +67,8 @@ export default class EventsForDay implements UiComponent {
.map(
(e) => new Event(
this._state.eventPatches
? new dto.PatchedEvent(e, this._state.eventPatches[e.datetime.utc().unix()])
: e,
? new dto.PatchedEvent(e as dto.Event, this._state.eventPatches[e.datetime.utc().unix()])

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought I'd chime in since I was watching @Nufflee work on this on stream. e as dto.Event is an unchecked cast and it is an invalid cast in this particular case. e is a plain javascript object that is defined in either of the returns on lines 41 or 57. Casting this object to dto.Event does not convert it into an instance of the class. However, this is an interesting situation because the application works correctly despite this bug. As passed to the constructor of PatchedEvent, the object instance is missing the timestamp() method and calling instance of Event on it would return false. What "fixes" this, is that the PatchedEvent dto class inherits from the Event dto class. The super call uses the properties of the passed in event object to instantiate a correct Event dto object which PatchedEvent inherits from, so the object from the invalid cast is never used anywhere else. This just so happens to work because the property names in the Event dto class happily line up with the property names in the returns in EventsForDay.ts (again, lines 41 and 57), but this is just "coincidence" - there is no type checking here.

Copy link
Contributor Author

@Nufflee Nufflee Feb 10, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see and I think I understand what you're talking about but what's the solution your suggesting? Actually creating a new instance instead of casting?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I may also add my opinion to this: by changing some of the dtos from interfaces to classes, the code is now written in two different paradigms: structural typing and OOP with prototype-based inheritance. Before, it was only structural typing, which I would argue is a better fit for the use case and there is no need to introduce OOP here, if patching events was handled like so:

type event_t = {
    datetime: number,
    title: string,
    description: string,
    url: string,
    channel: string
}

type event_patch_t = Partial<Omit<event_t, "datetime">>;

function patch_event(
    event: event_t,
    patch: event_patch_t): event_t {
    return { ...event, ...patch };
}

let abcd_event: event_t = {
    datetime: -1,
    title: 'abcd',
    description: 'description',
    url: 'https://google.com',
    channel: 'abcd'
}

console.log(patch_event(abcd_event, { title: 'patched' }));

However, obviously, this is not my codebase, so please treat this as a suggestion.

: e as dto.Event,
this._state.cancelledEvents
)
)
Expand Down
9 changes: 2 additions & 7 deletions ts/FutureEvent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import * as moment from 'moment';
import ComponentsArray from './ComponentsArray';
import Countdown from './Countdown';
import UiComponent from './UiComponent';
import Timestamp from './Timestamp';

export default class FutureEvent implements UiComponent {
constructor(private _event: dto.Event) {
Expand All @@ -12,13 +13,7 @@ export default class FutureEvent implements UiComponent {
appendTo(entry: HTMLElement | null): void {
new html.Div(
new ComponentsArray([
new html.Div(
new html.Href(
`#_${this._event.datetime.utc().unix()}`,
new html.Text(`${this._event.datetime.utc().unix()}`)
),
{"class": "timestamp"}
),
new Timestamp(this._event.timestamp()),
new html.H1(
new html.Href(
`${this._event.url}`,
Expand Down
9 changes: 2 additions & 7 deletions ts/PastEvent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import * as moment from 'moment';
import ComponentsArray from './ComponentsArray';
import Countdown from './Countdown';
import UiComponent from './UiComponent';
import Timestamp from './Timestamp';

export default class PastEvent implements UiComponent {
constructor(private _event: dto.Event) {
Expand All @@ -12,13 +13,7 @@ export default class PastEvent implements UiComponent {
appendTo(entry: HTMLElement | null): void {
new html.Div(
new ComponentsArray([
new html.Div(
new html.Href(
`#_${this._event.datetime.utc().unix()}`,
new html.Text(`${this._event.datetime.utc().unix()}`)
),
{"class": "timestamp"}
),
new Timestamp(this._event.timestamp()),
new html.H1(
new html.Href(
`${this._event.url}`,
Expand Down
35 changes: 35 additions & 0 deletions ts/Timestamp.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import * as dto from './dto';
import * as html from './html';
import * as moment from 'moment';
import ComponentsArray from './ComponentsArray';
import Countdown from './Countdown';
import UiComponent from './UiComponent';
import TwitchPlayer from './TwitchPlayer';
import copyToClipboard from './util/copyToClipboard';

export default class Timestamp implements UiComponent {
constructor(private _timestamp: string) {
}

onCopyClick = () => {
copyToClipboard(this._timestamp)
}

appendTo(entry: HTMLElement | null): void {
new html.Div(
new ComponentsArray([
new html.Href(
`#_${this._timestamp}`,
new html.Text(`${this._timestamp}`)
),
new html.Tag(
"i",
new html.Empty(),
{"class": "copy fas fa-copy fa-lg"},
{"click": this.onCopyClick}
)
]),
{"class": "timestamp"}
).appendTo(entry);
}
}
17 changes: 11 additions & 6 deletions ts/dto/Event.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
import * as moment from 'moment';

export default interface Event {
datetime: moment.Moment,
title: string,
description: string,
url: string,
channel: string,
export default class Event {
constructor(public datetime: moment.Moment,
public title: string,
public description: string,
public url: string,
public channel: string) {
}

timestamp() {
return this.datetime.utc().unix().toString();
}
}
14 changes: 2 additions & 12 deletions ts/dto/PatchedEvent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,9 @@ import * as moment from 'moment';
import Event from './Event';
import EventPatch from './EventPatch';

export default class PatchedEvent implements Event {
datetime: moment.Moment;
title: string;
description: string;
url: string;
channel: string;

export default class PatchedEvent extends Event {
constructor(event: Event, eventPatch: EventPatch | undefined) {
this.datetime = event.datetime;
this.title = event.title;
this.description = event.description;
this.url = event.url;
this.channel = event.channel;
super(event.datetime, event.title, event.description, event.url, event.channel);

if (eventPatch) {
if (eventPatch.title) {
Expand Down
9 changes: 8 additions & 1 deletion ts/html/Tag.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ import UiComponent from '../UiComponent';
export default class Tag implements UiComponent {
constructor(private _name: string,
private _body?: UiComponent,
private _attrs?: {[key: string]: any}) {
private _attrs?: {[key: string]: any},
private _events?: {[key: string]: EventListenerOrEventListenerObject}) {
}

appendTo(entry: HTMLElement | null): void {
Expand All @@ -21,6 +22,12 @@ export default class Tag implements UiComponent {
}
}

if (this._events) {
for (let eventKey in this._events) {
element.addEventListener(eventKey, this._events[eventKey])
}
}

if (this._body) {
this._body.appendTo(element);
}
Expand Down
49 changes: 49 additions & 0 deletions ts/util/copyToClipboard.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
// Adapted from https://github.com/feross/clipboard-copy

export default function copyToClipboard(text: string) {
// Use the Async Clipboard API when available. Requires a secure browing context (i.e. HTTPS)
Nufflee marked this conversation as resolved.
Show resolved Hide resolved
if ((navigator as any).clipboard) {
return (navigator as any).clipboard.writeText(text)
}

// ...Otherwise, use document.execCommand() fallback

// Put the text to copy into a <span>
const span = document.createElement('span')
span.textContent = text

// Preserve consecutive spaces and newlines
span.style.whiteSpace = 'pre'

// Add the <span> to the page
document.body.appendChild(span)

// Make a selection object representing the range of text selected by the user
const selection = window.getSelection()
const range = window.document.createRange()

if (!selection) {
return Promise.reject()
}

selection.removeAllRanges()
range.selectNode(span)
selection.addRange(range)

// Copy text to the clipboard
let success = false
try {
success = window.document.execCommand('copy')
} catch (err) {
console.log('Failed to copy text. Error: ', err)
}

// Cleanup
selection.removeAllRanges()
window.document.body.removeChild(span)

// The Async Clipboard API returns a promise that may reject with `undefined` so we match that here for consistency.
return success
? Promise.resolve()
: Promise.reject()
}