Full styleguide now available via `yarn styleguide`
Due to a number of hacks, the style guide can be used to show Backbone views. This will allow a smooth path from the old way of doing things to the new.pull/1/head
parent
893fb1cb9e
commit
1326b26585
@ -0,0 +1,6 @@
|
|||||||
|
|
||||||
|
```jsx
|
||||||
|
<util.MessageParents theme="android">
|
||||||
|
<Message />
|
||||||
|
</util.MessageParents>
|
||||||
|
```
|
@ -0,0 +1,36 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A placeholder Message component, giving the structure of a plain message with none of
|
||||||
|
* the dynamic functionality. We can build off of this going forward.
|
||||||
|
*/
|
||||||
|
export class Message extends React.Component<{}, {}> {
|
||||||
|
public render() {
|
||||||
|
return (
|
||||||
|
<li className="entry outgoing sent delivered">
|
||||||
|
<span className="avatar" />
|
||||||
|
<div className="bubble">
|
||||||
|
<div className="sender" dir="auto" />
|
||||||
|
<div className="attachments" />
|
||||||
|
<p className="content" dir="auto">
|
||||||
|
<span className="body">
|
||||||
|
Hi there. How are you doing? Feeling pretty good? Awesome.
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
<div className="meta">
|
||||||
|
<span
|
||||||
|
className="timestamp"
|
||||||
|
data-timestamp="1522800995425"
|
||||||
|
title="Tue, Apr 3, 2018 5:16 PM"
|
||||||
|
>
|
||||||
|
1 minute ago
|
||||||
|
</span>
|
||||||
|
<span className="status hide" />
|
||||||
|
<span className="timer" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,2 @@
|
|||||||
|
|
||||||
|
This is Reply.md.
|
@ -0,0 +1,14 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
|
||||||
|
interface IProps { name: string; }
|
||||||
|
|
||||||
|
interface IState { count: number; }
|
||||||
|
|
||||||
|
export class Reply extends React.Component<IProps, IState> {
|
||||||
|
public render() {
|
||||||
|
return (
|
||||||
|
<div>Placeholder</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -1,38 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
|
|
||||||
interface IProps { name: string; }
|
|
||||||
|
|
||||||
interface IState { count: number; }
|
|
||||||
|
|
||||||
|
|
||||||
const items = [
|
|
||||||
'one',
|
|
||||||
'two',
|
|
||||||
'three',
|
|
||||||
'four',
|
|
||||||
];
|
|
||||||
|
|
||||||
export class InlineReply extends React.Component<IProps, IState> {
|
|
||||||
constructor(props: IProps) {
|
|
||||||
super(props);
|
|
||||||
|
|
||||||
this.state = {
|
|
||||||
count: 0,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
public render() {
|
|
||||||
const { name } = this.props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
This is a basic component. Hi there, {name}!
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function greeter2(person: any) {
|
|
||||||
// console.log(items);
|
|
||||||
return `Hello, ${person}`;
|
|
||||||
}
|
|
@ -1,7 +0,0 @@
|
|||||||
import { InlineReply } from './sub/test2';
|
|
||||||
|
|
||||||
// console.log(InlineReply);
|
|
||||||
|
|
||||||
export function greeter(person: any) {
|
|
||||||
return 'Hello, ' + person;
|
|
||||||
}
|
|
@ -0,0 +1,20 @@
|
|||||||
|
Rendering a real `Whisper.MessageView` using `<util.MessageParents />` and
|
||||||
|
`<util.BackboneWrapper />`.
|
||||||
|
|
||||||
|
```jsx
|
||||||
|
const model = new Whisper.Message({
|
||||||
|
type: 'outgoing',
|
||||||
|
body: 'text',
|
||||||
|
sent_at: Date.now() - 5000,
|
||||||
|
})
|
||||||
|
const View = Whisper.MessageView;
|
||||||
|
const options = {
|
||||||
|
model,
|
||||||
|
};
|
||||||
|
<util.MessageParents theme="android">
|
||||||
|
<util.BackboneWrapper
|
||||||
|
View={View}
|
||||||
|
options={options}
|
||||||
|
/>
|
||||||
|
</util.MessageParents>
|
||||||
|
```
|
@ -0,0 +1,78 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
interface IProps {
|
||||||
|
/** The View class, which will be instantiated then treated like a Backbone View */
|
||||||
|
readonly View: IBackboneViewConstructor;
|
||||||
|
/** Options to be passed along to the view when constructed */
|
||||||
|
readonly options: object;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IBackboneView {
|
||||||
|
remove: () => void;
|
||||||
|
render: () => void;
|
||||||
|
el: HTMLElement;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IBackboneViewConstructor {
|
||||||
|
new (options: object): IBackboneView;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Allows Backbone Views to be rendered inside of React (primarily for the styleguide)
|
||||||
|
* while we slowly replace the internals of a given Backbone view with React.
|
||||||
|
*/
|
||||||
|
export class BackboneWrapper extends React.Component<IProps, {}> {
|
||||||
|
protected el: Element | null;
|
||||||
|
protected view: IBackboneView | null;
|
||||||
|
protected setEl: (element: HTMLDivElement | null) => void;
|
||||||
|
|
||||||
|
constructor(props: IProps) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.el = null;
|
||||||
|
this.view = null;
|
||||||
|
|
||||||
|
this.setEl = (element: HTMLDivElement | null) => {
|
||||||
|
this.el = element;
|
||||||
|
this.setup();
|
||||||
|
};
|
||||||
|
this.setup = this.setup.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
public setup() {
|
||||||
|
const { el } = this;
|
||||||
|
const { View, options } = this.props;
|
||||||
|
|
||||||
|
if (!el) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.view = new View(options);
|
||||||
|
this.view.render();
|
||||||
|
|
||||||
|
// It's important to let the view create its own root DOM element. This ensures that
|
||||||
|
// its tagName property actually takes effect.
|
||||||
|
el.appendChild(this.view.el);
|
||||||
|
}
|
||||||
|
|
||||||
|
public teardown() {
|
||||||
|
if (!this.view) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.view.remove();
|
||||||
|
this.view = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public componentWillUnmount() {
|
||||||
|
this.teardown();
|
||||||
|
}
|
||||||
|
|
||||||
|
public shouldComponentUpdate() {
|
||||||
|
// we're handling all updates manually
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public render() {
|
||||||
|
return <div ref={this.setEl} />;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,8 @@
|
|||||||
|
|
||||||
|
The simplest example of using the `<MessagesParents />` component:
|
||||||
|
|
||||||
|
```jsx
|
||||||
|
<util.MessageParents theme="android">
|
||||||
|
<div>Just a plain bit of text</div>
|
||||||
|
</util.MessageParents>
|
||||||
|
```
|
@ -0,0 +1,31 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
|
||||||
|
interface IProps {
|
||||||
|
/**
|
||||||
|
* Corresponds to the theme setting in the app, and the class added to the root element.
|
||||||
|
*/
|
||||||
|
theme: "ios" | "android" | "android-dark";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides the parent elements necessary to allow the main Signal Desktop stylesheet to
|
||||||
|
* apply (with no changes) to messages in this context.
|
||||||
|
*/
|
||||||
|
export class MessageParents extends React.Component<IProps, {}> {
|
||||||
|
public render() {
|
||||||
|
const { theme } = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={theme}>
|
||||||
|
<div className="conversation">
|
||||||
|
<div className="discussion-container" style={{padding: '0.5em'}}>
|
||||||
|
<ul className="message-list">
|
||||||
|
{this.props.children}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,24 @@
|
|||||||
|
// Helper components used in the styleguide, exposed at 'util' in the global scope via the
|
||||||
|
// context option in reaat-styleguidist.
|
||||||
|
|
||||||
|
export { MessageParents } from './MessageParents';
|
||||||
|
export { BackboneWrapper } from './BackboneWrapper';
|
||||||
|
|
||||||
|
// Here we can make things inside Webpack available to Backbone views like preload.js.
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import ReactDOM from 'react-dom';
|
||||||
|
|
||||||
|
import { Message } from '../conversation/Message';
|
||||||
|
import { Reply } from '../conversation/Reply';
|
||||||
|
|
||||||
|
// Required, or TypeScript complains about adding keys to window
|
||||||
|
const parent = window as any;
|
||||||
|
|
||||||
|
parent.React = React;
|
||||||
|
parent.ReactDOM = ReactDOM;
|
||||||
|
|
||||||
|
const SignalReact = parent.Signal.React = parent.Signal.React || {};
|
||||||
|
|
||||||
|
SignalReact.Message = Message;
|
||||||
|
SignalReact.Reply = Reply;
|
@ -0,0 +1,47 @@
|
|||||||
|
/* global Backbone: false */
|
||||||
|
|
||||||
|
// Additional globals used:
|
||||||
|
// window.React
|
||||||
|
// window.ReactDOM
|
||||||
|
// window.i18n
|
||||||
|
|
||||||
|
// eslint-disable-next-line func-names
|
||||||
|
(function () {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
window.Whisper = window.Whisper || {};
|
||||||
|
|
||||||
|
window.Whisper.ReactWrapper = Backbone.View.extend({
|
||||||
|
className: 'react-wrapper',
|
||||||
|
initialize(options) {
|
||||||
|
const { Component, props, onClose } = options;
|
||||||
|
this.render();
|
||||||
|
|
||||||
|
this.Component = Component;
|
||||||
|
this.onClose = onClose;
|
||||||
|
|
||||||
|
this.update(props);
|
||||||
|
},
|
||||||
|
update(props) {
|
||||||
|
const updatedProps = this.augmentProps(props);
|
||||||
|
const element = window.React.createElement(this.Component, updatedProps);
|
||||||
|
window.ReactDOM.render(element, this.el);
|
||||||
|
},
|
||||||
|
augmentProps(props) {
|
||||||
|
return Object.assign({}, props, {
|
||||||
|
close: () => {
|
||||||
|
if (this.onClose) {
|
||||||
|
this.onClose();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.remove();
|
||||||
|
},
|
||||||
|
i18n: window.i18n,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
remove() {
|
||||||
|
window.ReactDOM.unmountComponentAtNode(this.el);
|
||||||
|
Backbone.View.prototype.remove.call(this);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}());
|
@ -0,0 +1,158 @@
|
|||||||
|
const webpack = require('webpack');
|
||||||
|
const path = require('path');
|
||||||
|
const typescriptSupport = require('react-docgen-typescript');
|
||||||
|
|
||||||
|
|
||||||
|
const propsParser = typescriptSupport.withCustomConfig('./tsconfig.json').parse;
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
sections: [
|
||||||
|
{
|
||||||
|
name: 'Conversation',
|
||||||
|
description: 'Everything necessary to render a conversation',
|
||||||
|
components: 'js/react/conversation/*.tsx',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Utility',
|
||||||
|
description: 'Utility components only used for testing',
|
||||||
|
components: 'js/react/util/*.tsx',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
context: {
|
||||||
|
// Exposes necessary utilities in the global scope for all readme code snippets
|
||||||
|
util: 'js/react/util',
|
||||||
|
},
|
||||||
|
// We don't want one long, single page
|
||||||
|
pagePerSection: true,
|
||||||
|
// Expose entire repository to the styleguidist server, primarily for stylesheets
|
||||||
|
assetsDir: './',
|
||||||
|
// Add top-level elements to the HTML:
|
||||||
|
// docs: https://github.com/vxna/mini-html-webpack-template
|
||||||
|
// https://react-styleguidist.js.org/docs/configuration.html#template
|
||||||
|
template: {
|
||||||
|
head: {
|
||||||
|
links: [{
|
||||||
|
rel: 'stylesheet',
|
||||||
|
type: 'text/css',
|
||||||
|
href: '/stylesheets/manifest.css',
|
||||||
|
}],
|
||||||
|
},
|
||||||
|
body: {
|
||||||
|
// Brings in all the necessary components to boostrap Backbone views
|
||||||
|
// Mirrors the order used in background.js.
|
||||||
|
scripts: [
|
||||||
|
{
|
||||||
|
src: 'test/legacy_bridge.js',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
src: 'node_modules/moment/min/moment-with-locales.min.js',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
src: 'js/components.js',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
src: 'js/reliable_trigger.js',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
src: 'js/database.js',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
src: 'js/storage.js',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
src: 'js/signal_protocol_store.js',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
src: 'js/libtextsecure.js',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
src: 'js/focus_listener.js',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
src: 'js/notifications.js',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
src: 'js/delivery_receipts.js',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
src: 'js/read_receipts.js',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
src: 'js/read_syncs.js',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
src: 'js/libphonenumber-util.js',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
src: 'js/models/messages.js',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
src: 'js/models/conversations.js',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
src: 'js/models/blockedNumbers.js',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
src: 'js/expiring_messages.js',
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
src: 'js/chromium.js',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
src: 'js/registration.js',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
src: 'js/expire.js',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
src: 'js/conversation_controller.js',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
src: 'js/emoji_util.js',
|
||||||
|
},
|
||||||
|
// Select Backbone views
|
||||||
|
{
|
||||||
|
src: 'js/views/whisper_view.js',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
src: 'js/views/timestamp_view.js',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
src: 'js/views/message_view.js',
|
||||||
|
},
|
||||||
|
// Hacky way of including templates for Backbone components
|
||||||
|
{
|
||||||
|
src: 'test/legacy_templates.js',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
propsParser,
|
||||||
|
webpackConfig: {
|
||||||
|
devtool: 'source-map',
|
||||||
|
|
||||||
|
resolve: {
|
||||||
|
// Necessary to enable the absolute path used in the context option above
|
||||||
|
modules: [
|
||||||
|
__dirname,
|
||||||
|
path.join(__dirname, 'node_modules'),
|
||||||
|
],
|
||||||
|
extensions: ['.tsx'],
|
||||||
|
},
|
||||||
|
|
||||||
|
module: {
|
||||||
|
rules: [
|
||||||
|
{
|
||||||
|
test: /\.tsx?$/,
|
||||||
|
loader: 'ts-loader'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// To test handling of attachments, we need arraybuffers in memory
|
||||||
|
test: /\.(gif|mp3|mp4)$/,
|
||||||
|
loader: 'arraybuffer-loader',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
@ -0,0 +1,42 @@
|
|||||||
|
|
||||||
|
// Because we aren't hosting the styleguide in Electron, we can't rely on preload.js
|
||||||
|
// to set things up for us. This gives us the minimum bar shims for everything it
|
||||||
|
// provdes.
|
||||||
|
//
|
||||||
|
// Remember, the idea here is just to enable visual testing, no full functionality. Most
|
||||||
|
// of thise can be very simple.
|
||||||
|
|
||||||
|
window.PROTO_ROOT = '/protos';
|
||||||
|
window.nodeSetImmediate = () => {};
|
||||||
|
|
||||||
|
window.Signal = {};
|
||||||
|
window.Signal.Backup = {};
|
||||||
|
window.Signal.Crypto = {};
|
||||||
|
window.Signal.Logs = {};
|
||||||
|
window.Signal.Migrations = {};
|
||||||
|
|
||||||
|
window.Signal.React = window.Signal.React = {};
|
||||||
|
|
||||||
|
window.EmojiConvertor = function EmojiConvertor() {};
|
||||||
|
window.EmojiConvertor.prototype.init_colons = () => {}
|
||||||
|
window.EmojiConvertor.prototype.signalReplace = html => html;
|
||||||
|
window.EmojiConvertor.prototype.replace_unified = string => string;
|
||||||
|
window.EmojiConvertor.prototype.img_sets = {
|
||||||
|
apple: {}
|
||||||
|
};
|
||||||
|
|
||||||
|
window.i18n = () => '';
|
||||||
|
|
||||||
|
window.Signal.Migrations.V17 = {};
|
||||||
|
window.Signal.OS = {};
|
||||||
|
window.Signal.Types = {};
|
||||||
|
window.Signal.Types.Attachment = {};
|
||||||
|
window.Signal.Types.Errors = {};
|
||||||
|
window.Signal.Types.Message = {
|
||||||
|
initializeSchemaVersion: attributes => attributes,
|
||||||
|
};
|
||||||
|
window.Signal.Types.MIME = {};
|
||||||
|
window.Signal.Types.Settings = {};
|
||||||
|
window.Signal.Views = {};
|
||||||
|
window.Signal.Views.Initialization = {};
|
||||||
|
window.Signal.Workflow = {};
|
@ -0,0 +1,45 @@
|
|||||||
|
|
||||||
|
// Taken from background.html.
|
||||||
|
// Templates are here solely to support the Backbone views rendered in the styleguide.
|
||||||
|
|
||||||
|
window.Whisper.View.Templates = {
|
||||||
|
hasRetry: `
|
||||||
|
{{ messageNotSent }}
|
||||||
|
<span href='#' class='retry'>{{ resend }}</span>
|
||||||
|
`,
|
||||||
|
'some-failed': `
|
||||||
|
{{ someFailed }}
|
||||||
|
`,
|
||||||
|
keychange: `
|
||||||
|
<span class='content' dir='auto'><span class='shield icon'></span> {{ content }}</span>
|
||||||
|
`,
|
||||||
|
'verified-change': `
|
||||||
|
<span class='content' dir='auto'><span class='{{ icon }} icon'></span> {{ content }}</span>
|
||||||
|
`,
|
||||||
|
message: `
|
||||||
|
{{> avatar }}
|
||||||
|
<div class='bubble {{ avatar.color }}'>
|
||||||
|
<div class='sender' dir='auto'>
|
||||||
|
{{ sender }}
|
||||||
|
{{ #profileName }}
|
||||||
|
<span class='profileName'>{{ profileName }} </span>
|
||||||
|
{{ /profileName }}
|
||||||
|
</div>
|
||||||
|
<div class='attachments'></div>
|
||||||
|
<p class='content' dir='auto'>
|
||||||
|
{{ #message }}<span class='body'>{{ message }}</span>{{ /message }}
|
||||||
|
</p>
|
||||||
|
<div class='meta'>
|
||||||
|
<span class='timestamp' data-timestamp={{ timestamp }}></span>
|
||||||
|
<span class='status hide'></span>
|
||||||
|
<span class='timer'></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
hourglass: `
|
||||||
|
<span class='hourglass'><span class='sand'></span></span>
|
||||||
|
`,
|
||||||
|
expirationTimerUpdate: `
|
||||||
|
<span class='content'><span class='icon clock'></span> {{ content }}</span>
|
||||||
|
`
|
||||||
|
};
|
Loading…
Reference in New Issue