Add basic github support to /connect

This commit is contained in:
Ben Halpern 2018-06-19 13:46:10 -04:00
parent da585b5f83
commit 914102f19f
12 changed files with 194 additions and 7 deletions

View file

@ -300,6 +300,29 @@
top:-4px;
}
.activecontent__githubrepo{
}
.activecontent__githubrepoheader{
padding-left: 30px;
padding-bottom: 3px;
min-height: 30px;
margin-top:-2px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.activecontent__githubrepofiles{
border-bottom: 1px solid $light-medium-gray;
font-size:0.8em;
}
.activecontent__githubrepofilerow{
padding: 4px 2px;
border-top: 1px solid $light-medium-gray;
}
.chatchannels {
display: flex;
flex-direction: column;

View file

@ -99,6 +99,7 @@ class ChatChannelsController < ApplicationController
@active_channel = ChatChannel.find_by_slug(slug)
@active_channel.current_user = current_user if @active_channel
end
generate_github_token
generate_algolia_search_key
end
@ -109,4 +110,8 @@ class ChatChannelsController < ApplicationController
ENV["ALGOLIASEARCH_SEARCH_ONLY_KEY"], params,
)
end
def generate_github_token
@github_token = Identity.where(user_id: current_user.id, provider: "github").first&.token
end
end

View file

@ -129,4 +129,15 @@ export function getContent(url, successCb, failureCb) {
.then(response => response.json())
.then(successCb)
.catch(failureCb);
}
export function getJSONContents(url, successCb, failureCb) {
fetch(url, {
Accept: 'application/json',
'Content-Type': 'application/json',
credentials: 'same-origin',
})
.then(response => response.json())
.then(successCb)
.catch(failureCb);
}

View file

@ -300,6 +300,11 @@ export default class Chat extends Component {
} else {
alert("Calls are only currently available in direct channels");
}
} else if (message.startsWith('/github')) {
const args = message.split('/github ')[1].trim()
let newActiveContent = this.state.activeContent
newActiveContent[this.state.activeChannelId] = {type_of: "github", args: args }
this.setState({activeContent: newActiveContent})
} else if (message[0] === '/') {
conductModeration(
this.state.activeChannelId,
@ -545,6 +550,7 @@ export default class Chat extends Component {
onExit={this.triggerActiveContent}
activeChannelId={this.state.activeChannelId}
pusherKey={this.props.pusherKey}
githubToken={this.props.githubToken}
/>
</div>
);

View file

@ -1,6 +1,7 @@
import { h, Component } from 'preact';
import PropTypes from 'prop-types';
import CodeEditor from './codeEditor';
import GithubRepo from './githubRepo';
export default class Content extends Component {
static propTypes = {
@ -72,6 +73,13 @@ function display(props) {
<div dangerouslySetInnerHTML={{__html: props.resource.body_html}} ></div>
</div>
</div>)
} else if (props.resource.type_of === "github") {
return <GithubRepo
activeChannelId={props.activeChannelId}
pusherKey={props.pusherKey}
githubToken={props.githubToken}
resource={props.resource}
/>
} else if (props.resource.type_of === "code_editor") {
return <CodeEditor
activeChannelId={props.activeChannelId}

View file

@ -0,0 +1,128 @@
import { h, Component } from 'preact';
import PropTypes from 'prop-types';
import { getJSONContents } from './actions';
import marked from 'marked';
export default class GithubRepo extends Component {
constructor(props) {
super(props);
this.state = {
root: true,
directories: [],
files: [],
readme: null,
content: null,
token: props.githubToken,
path: null
}
}
componentDidMount(){
if (this.state.token) {
getJSONContents(`https://api.github.com/repos/${this.props.resource.args}/contents?access_token=`+this.state.token,
this.loadContent,
this.loadFailure)
getJSONContents(`https://api.github.com/repos/${this.props.resource.args}/readme?access_token=`+this.state.token,
this.loadContent,
this.loadFailure)
}
this.setState({path: this.props.resource.args})
}
handleItemClick = e => {
e.preventDefault();
getJSONContents(`${e.target.dataset.apiUrl}&access_token=`+this.state.token,
this.loadContent,
this.loadFailure)
this.setState({
root: false,
path: e.target.dataset.path
})
}
loadContent = response => {
let files = [];
let directories = [];
console.log(response)
if (response.message === 'Not Found') {
this.setState({path: 'Repo not found (misspelled or private?)'})
} else if (Array.isArray(response)) {
response.forEach( item => {
if (item.type === 'file') {
files.push(item)
} else {
directories.push(item)
}
})
this.setState({files: files,
directories: directories,
response: response})
} else if (response.path === "README.md") {
this.setState({
readme: window.atob(response.content)
})
} else if (response.content) {
this.setState({
content: window.atob(response.content),
response: response
})
}
}
loadFailure = response => {
this.setState({path: response.message})
}
render() {
if (!this.state.token || this.state.token.length === 0) {
return <div className='activecontent__githubrepo'>
<div className='activecontent__githubrepoheader'><em>Authentication required</em></div>
<p>
You must <a href='/users/auth/github' data-no-instant>authenticate with GitHub</a> to use this feature.
</p>
</div>
} else if (this.state.content) {
return <div className='activecontent__githubrepo'>
<div className='activecontent__githubrepoheader'>{this.state.path}</div>
<pre>
{this.state.content}
</pre>
</div>
} else {
const directories = this.state.directories.map( item => {
return <div className='activecontent__githubrepofilerow'>
<a href={item.html_url} data-api-url={item.url} data-path={item.path} onClick={this.handleItemClick}>📁 {item.name}</a>
</div>
});
const files = this.state.files.map( item => {
return <div className='activecontent__githubrepofilerow'>
<a href={item.html_url} data-api-url={item.url} data-path={item.path} onClick={this.handleItemClick}>{item.name}</a>
</div>
});
let readme = ''
if (this.state.readme) {
readme = <div dangerouslySetInnerHTML={{__html: marked(this.state.readme)}} ></div>
}
if (this.state.root) {
return <div className='activecontent__githubrepo'>
<div className='activecontent__githubrepoheader'><a href="/Users/benhalpern/dev/dev.to_core/app"></a>{this.state.path}</div>
<div className='activecontent__githubrepofiles'>
{directories}
{files}
</div>
{readme}
</div>
} else {
return <div className='activecontent__githubrepo'>
<div className='activecontent__githubrepoheader'>{this.state.path}</div>
<div className='activecontent__githubrepofiles'>
{directories}
{files}
</div>
</div>
}
}
}
}

View file

@ -13,7 +13,7 @@ HTMLDocument.prototype.ready = new Promise(resolve => {
document.ready.then(
getUserDataAndCsrfToken().then(currentUser => {
if (document.getElementById('chat')) {
const { chatChannels, pusherKey, chatOptions, algoliaKey, algoliaId, algoliaIndex, twilioToken } = document.getElementById(
const { chatChannels, pusherKey, chatOptions, algoliaKey, algoliaId, algoliaIndex, githubToken } = document.getElementById(
'chat',
).dataset;
window.currentUser = currentUser;
@ -30,6 +30,7 @@ document.ready.then(
algoliaId={algoliaId}
algoliaKey={algoliaKey}
algoliaIndex={algoliaIndex}
githubToken={githubToken}
/>,
renderTarget, renderTarget.firstChild
);

View file

@ -18,6 +18,7 @@
data-algolia-id="<%= ENV["ALGOLIASEARCH_APPLICATION_ID"] %>"
data-algolia-key="<%= @secured_algolia_key %>"
data-algolia-index="<%= "SecuredChatChannel_#{Rails.env}" %>"
data-github-token="<%= @github_token %>"
data-chat-channels="<%= [] %>"
data-chat-options="<%= {showChannelsList:true, showTimestamp: true, activeChannelId: @active_channel&.id }.to_json %>"
>

View file

@ -18,7 +18,7 @@
key: "video-upload__#{SecureRandom.hex}",
key_starts_with: "video-upload__",
acl: "public-read",
max_file_size: 2500.megabytes,
max_file_size: (current_user.has_role?(:super_admin) ? 10000 : 3500).megabytes,
id: "s3-uploader",
class: "upload-form",
data: {:key => :val} do %>

View file

@ -18,7 +18,7 @@ end
p "2/7 Creating users"
roles = %i(level_1_member level_2_member level_3_member level_4_member workshop_pass)
80.times do |i|
40.times do |i|
begin
identity_attributes = { provider: "twitter",
uid: "#{i}",
@ -123,7 +123,7 @@ Tag.create!(
)
p "4/7 Creating articles"
150.times do
80.times do
# begin
four_tags_string = "discuss, meta, git, changelog"
valid_markdown = "---\ntitle: #{Faker::Book.title} #{rand(5000)}\npublished: true\ncover_image: #{Faker::Avatar.image}\ntags: #{four_tags_string}\n---\n#{Faker::Hipster.paragraph(4)}"
@ -142,11 +142,11 @@ end
# Create comments
p "5/7 Creating comments"
200.times do
120.times do
attributes = {
body_markdown: Faker::Hipster.paragraph(1),
user_id: User.order("RANDOM()").first.id,
commentable_id: Article.order("RANDOM()").first.id,
commentable_id: Article.where("id > ?", 30).order("RANDOM()").first.id,
commentable_type: "Article",
}
Comment.create!(attributes)
@ -164,7 +164,6 @@ podcast_objects = [
podcast_objects.each do |attributes|
podcast = Podcast.create!(attributes)
PodcastFeed.new.get_episodes(podcast, 4)
end
p "7/7 Creating Broadcasts"

View file

@ -82,6 +82,7 @@
"@webscopeio/react-textarea-autocomplete": "^2.2.0",
"codemirror": "^5.38.0",
"intersection-observer": "^0.5.0",
"marked": "^0.4.0",
"preact": "^8.2.5",
"prop-types": "^15.6.0",
"pusher-js": "^4.2.2",

View file

@ -6134,6 +6134,10 @@ marked@^0.3.9:
version "0.3.18"
resolved "https://registry.yarnpkg.com/marked/-/marked-0.3.18.tgz#3ef058cd926101849b92a7a7c15db18c7fc76b2f"
marked@^0.4.0:
version "0.4.0"
resolved "https://registry.yarnpkg.com/marked/-/marked-0.4.0.tgz#9ad2c2a7a1791f10a852e0112f77b571dce10c66"
math-expression-evaluator@^1.2.14:
version "1.2.17"
resolved "https://registry.yarnpkg.com/math-expression-evaluator/-/math-expression-evaluator-1.2.17.tgz#de819fdbcd84dccd8fae59c6aeb79615b9d266ac"