diff --git a/package.json b/package.json index 5c9a67c734..a736024da6 100644 --- a/package.json +++ b/package.json @@ -38,7 +38,8 @@ "react-dom": "^15.0.1", "react-gemini-scrollbar": "matrix-org/react-gemini-scrollbar#c3d942e", "sanitize-html": "^1.11.1", - "velocity-vector": "vector-im/velocity#059e3b2" + "velocity-vector": "vector-im/velocity#059e3b2", + "whatwg-fetch": "^1.0.0" }, "//babelversion": [ "brief experiments with babel6 seems to show that it generates source ", diff --git a/src/MatrixClientPeg.js b/src/MatrixClientPeg.js index cc96503316..c7b77ab88c 100644 --- a/src/MatrixClientPeg.js +++ b/src/MatrixClientPeg.js @@ -20,7 +20,7 @@ limitations under the License. var Matrix = require("matrix-js-sdk"); var GuestAccess = require("./GuestAccess"); -var matrixClient = null; +let matrixClient: MatrixClient = null; var localStorage = window.localStorage; @@ -82,7 +82,7 @@ class MatrixClient { this.guestAccess = guestAccess; } - get() { + get(): MatrixClient { return matrixClient; } diff --git a/src/autocomplete/AutocompleteProvider.js b/src/autocomplete/AutocompleteProvider.js index 3b2aae920b..61158d2b56 100644 --- a/src/autocomplete/AutocompleteProvider.js +++ b/src/autocomplete/AutocompleteProvider.js @@ -1,3 +1,5 @@ export default class AutocompleteProvider { - + getName(): string { + return 'Default Provider'; + } } diff --git a/src/autocomplete/Autocompleter.js b/src/autocomplete/Autocompleter.js index e49dbb7ad6..a8ed2da59a 100644 --- a/src/autocomplete/Autocompleter.js +++ b/src/autocomplete/Autocompleter.js @@ -1,7 +1,20 @@ import CommandProvider from './CommandProvider'; +import DuckDuckGoProvider from './DuckDuckGoProvider'; +import RoomProvider from './RoomProvider'; +import UserProvider from './UserProvider'; -const COMPLETERS = [CommandProvider].map(completer => new completer()); +const PROVIDERS = [ + CommandProvider, + DuckDuckGoProvider, + RoomProvider, + UserProvider +].map(completer => new completer()); export function getCompletions(query: String) { - return COMPLETERS.map(completer => completer.getCompletions(query)); + return PROVIDERS.map(provider => { + return { + completions: provider.getCompletions(query), + provider + }; + }); } diff --git a/src/autocomplete/CommandProvider.js b/src/autocomplete/CommandProvider.js index cc95d96fd3..e2eac47d16 100644 --- a/src/autocomplete/CommandProvider.js +++ b/src/autocomplete/CommandProvider.js @@ -62,4 +62,8 @@ export default class CommandProvider extends AutocompleteProvider { } return Q.when(completions); } + + getName() { + return 'Commands'; + } } diff --git a/src/autocomplete/DuckDuckGoProvider.js b/src/autocomplete/DuckDuckGoProvider.js new file mode 100644 index 0000000000..6545b96cbd --- /dev/null +++ b/src/autocomplete/DuckDuckGoProvider.js @@ -0,0 +1,35 @@ +import AutocompleteProvider from './AutocompleteProvider'; +import Q from 'q'; +import 'whatwg-fetch'; + +const DDG_REGEX = /\/ddg\w+(.+)$/; +const REFERER = 'vector'; + +export default class DuckDuckGoProvider extends AutocompleteProvider { + static getQueryUri(query: String) { + return `http://api.duckduckgo.com/?q=${encodeURIComponent(query)}&format=json&t=${encodeURIComponent(REFERER)}`; + } + + getCompletions(query: String) { + if(!query) + return Q.when([]); + + let promise = Q.defer(); + fetch(DuckDuckGoProvider.getQueryUri(query), { + method: 'GET' + }).then(response => { + let results = response.Results.map(result => { + return { + title: result.Text, + description: result.Result + }; + }); + promise.resolve(results); + }); + return promise; + } + + getName() { + return 'Results from DuckDuckGo'; + } +} diff --git a/src/autocomplete/RoomProvider.js b/src/autocomplete/RoomProvider.js new file mode 100644 index 0000000000..26dc5733da --- /dev/null +++ b/src/autocomplete/RoomProvider.js @@ -0,0 +1,31 @@ +import AutocompleteProvider from './AutocompleteProvider'; +import Q from 'q'; +import MatrixClientPeg from '../MatrixClientPeg'; + +const ROOM_REGEX = /(?=#)[^\s]*/g; + +export default class RoomProvider extends AutocompleteProvider { + constructor() { + super(); + } + + getCompletions(query: String) { + let client = MatrixClientPeg.get(); + let completions = []; + const matches = query.match(ROOM_REGEX); + if(!!matches) { + const command = matches[0]; + completions = client.getRooms().map(room => { + return { + title: room.name, + subtitle: room.roomId + }; + }); + } + return Q.when(completions); + } + + getName() { + return 'Rooms'; + } +} diff --git a/src/autocomplete/UserProvider.js b/src/autocomplete/UserProvider.js new file mode 100644 index 0000000000..791dd55a33 --- /dev/null +++ b/src/autocomplete/UserProvider.js @@ -0,0 +1,31 @@ +import AutocompleteProvider from './AutocompleteProvider'; +import Q from 'q'; +import MatrixClientPeg from '../MatrixClientPeg'; + +const ROOM_REGEX = /@[^\s]*/g; + +export default class UserProvider extends AutocompleteProvider { + constructor() { + super(); + } + + getCompletions(query: String) { + let client = MatrixClientPeg.get(); + let completions = []; + const matches = query.match(ROOM_REGEX); + if(!!matches) { + const command = matches[0]; + completions = client.getUsers().map(user => { + return { + title: user.displayName, + description: user.userId + }; + }); + } + return Q.when(completions); + } + + getName() { + return 'Users'; + } +} diff --git a/src/components/views/rooms/Autocomplete.js b/src/components/views/rooms/Autocomplete.js index 80208892b0..4bc4102070 100644 --- a/src/components/views/rooms/Autocomplete.js +++ b/src/components/views/rooms/Autocomplete.js @@ -11,11 +11,28 @@ export default class Autocomplete extends React.Component { } componentWillReceiveProps(props, state) { - getCompletions(props.query)[0].then(completions => { - console.log(completions); - this.setState({ - completions - }); + getCompletions(props.query).map(completionResult => { + try { + completionResult.completions.then(completions => { + let i = this.state.completions.findIndex( + completion => completion.provider === completionResult.provider + ); + + i = i == -1 ? this.state.completions.length : i; + console.log(completionResult); + let newCompletions = Object.assign([], this.state.completions); + completionResult.completions = completions; + newCompletions[i] = completionResult; + console.log(newCompletions); + this.setState({ + completions: newCompletions + }); + }, err => { + + }); + } catch (e) { + // An error in one provider shouldn't mess up the rest. + } }); } @@ -33,18 +50,28 @@ export default class Autocomplete extends React.Component { }; this.props.pinTo.forEach(direction => { - console.log(`${direction} = ${position[direction]}`); style[direction] = position[direction]; }); - const renderedCompletions = this.state.completions.map((completion, i) => { - return ( - <div key={i} class="mx_Autocomplete_Completion"> - <strong>{completion.title}</strong> - <em>{completion.subtitle}</em> - <span style={{color: 'gray', float: 'right'}}>{completion.description}</span> + const renderedCompletions = this.state.completions.map((completionResult, i) => { + console.log(completionResult); + let completions = completionResult.completions.map((completion, i) => { + return ( + <div key={i} class="mx_Autocomplete_Completion"> + <strong>{completion.title}</strong> + <em>{completion.subtitle}</em> + <span style={{color: 'gray', float: 'right'}}>{completion.description}</span> + </div> + ); + }); + + + return completions.length > 0 ? ( + <div key={i} class="mx_Autocomplete_ProviderSection"> + <strong>{completionResult.provider.getName()}</strong> + {completions} </div> - ); + ) : null; }); return (