conversejs / community-plugins Goto Github PK
View Code? Open in Web Editor NEW3rd party, community contributed plugins for Converse.js
License: Other
3rd party, community contributed plugins for Converse.js
License: Other
This is a desirable feature of Telegram that is needed in Converse.
Converse keeps a record of the different types of user shared content and it shows them in a list. Making much easier to locate what was posted later on.
If you ever looked for a link you posted on a chat room you'll find this will help you alot.
This could be either a plugin or a feature in converse
I have an admin inteface, and would really like to
Is this possible?
At some point on version 7.x I had the css code below, however it had various problems and is completely broken in 8.0.
My basic objective as you can see was to only used the area in the fixed left column of the window for the small chat toggles, going in an upwards direction.
.chat-head-chatroom > .close-chatbox-button {
display: none !important;
}
#conversejs.converse-overlayed > .row {
flex-direction: column-reverse !important;
}
#conversejs .converse-chatboxes {
position: fixed !important;
bottom: 40px !important;
width: 140px !important;
left: 0px !important;
right: unset !important;
}
#conversejs .converse-chatboxes {
left: 0 !important;
right: unset !important;
}
#conversejs .chatbox-btn.close-chatbox-button.fa-sign-out-alt {
display: none;
}
#conversejs.converse-overlayed .toggle-controlbox {
margin: 4px 10px 0 4px !important;
}
#conversejs.converse-overlayed #minimized-chats {
margin-left: 4px !important;
}
#conversejs.converse-overlayed #minimized-chats .minimized-chats-flyout {
flex-direction: column-reverse !important;
bottom: 100px !important;
}
In my tests with the settings plugin I noticed that it seems to show a blank state, so it is not possible to see what current settings (for example as defined in the index.html) are already set. This seems to me like a vital function, no?
Furthermore there are a lot of settings that I would not expose to the end user. Is there some plan to hide them?
Thanks!
I see there is such feature on Pade, I was wondering, if it would be easy for you @deleolajide to port it to converse. I love looks of Pade, including the default when no avatar is given (there was also a random color assigned to them right?).
Might be good to have something similar to Riot.im, i.e. a somewhat customizable portal page (to add some links or a small explanation) that also shows the locally available public MUCs and allows non-logged in reading of (some of) the available MUCs on the linked primary server.
This was really a good plugin . but some time it was not working any workaround to overcome this issue even given full matched word not results displaying
Right now even on a 1280 wide screen some of the settings descriptions are cut short due to the lack of word-wrap.
Also the left side-bar of the settings should probably auto-collapse into a hamburger menu or something like that on a small screen like a mobile phone.
Thanks for looking into this :)
Firefox 66
@deleolajide Was this tested only in Chrome?
HI,
we integrated search plugin in to our converse application. and can see in console search plugin was ready . but missing search icon . can you guide how to acheive this
For plugins that load files dynamically, provide plugins with a root path setting to enable them be used in different folder structures.
...but working with 4.2.0
Will try to pinpoint the exact commit.
Hello,
vmsg is looking for index.html in this path: https://woodpeckersnest.space:5281/packages/vmsg/index.html
while my plugins are under https://woodpeckersnest.space:5281/conversejs/dist/plugins/vmsg/vmsg.js
So it returns a 404. I'm using mod_conversejs from prosody.
I manually edited "vmsg.js" at line 34, changed the index.html path and now I have a different problem:
Error: TypeError: Failed to execute 'compile' on 'WebAssembly': Incorrect response MIME type. Expected 'application/wasm'.
at worker.onmessage (VM578 vmsg-lib.js:238:18)
There are no licenses, all copyright? :(
Inside the overlay modal it just shows a standard 404 error message and the following in the browser console:
TypeError: this.el.querySelector(...).contentWindow.getMp3File is not a function vmsg.js:42:93
Would be convenient to be able to configure the audioconference server settings easily in the main html document instead of having to modify the javascript file.
An automatic fallback attempt to the main specified BOSH (ws?) server would be also nice in case no server is specified.
I already have conversejs v9.0.0 working with core features. But I needed to add jitsimeet for audio and video call purpose so I added jitsimeet.js file just to check if its working.
I am getting video call icon in toolbar buttons but getting error in console.
Please check below screenshot for more details. And let me know if any more information required.
Can anyone help me with any solution to this issue?
@jcbrand @deleolajide @SilverYoCha
For easier mobile or tablet use it would be cool to have an app to directly snap a picture and send it.
Maybe this could be used:
https://github.com/kasperkamperman/MobileCameraTemplate
I have visible_toolbar_buttons: {emoji: false},
set on my mobile view-mode instance as it is broken on small screens and the OSK comes with it's own smilies, but when I try to enable the search plugin it gives the following error:
TypeError: smiley is null search.js:175:13
With that it still loads into the control box but I can join any chats or MUCs.
There are a few breaking changes to plugins with Converse 5.0.5. Fix them :-)
https://github.com/kern/filepizza
Might be possible to use as an ConverseJS addon.
Hi everyone,
May be the answer of my question is simple. But do no know how to install a new plugin.
I used to compile the convers.js (make dist) form source code. So How can I add some community-plugins in the source before compilation. I such a way that after compile (make dist) I will be able to whitelist of blacklist extra plugins based on my needs.
Regards,
Since Pade seems to have XEP-0070 location sharing support, it would be cool to have that also available in vanilla ConverseJS.
Hi there,
using mod_conversejs with prosody, I got the following issue with voicechat plugin:
startVoiceChat ReferenceError: $ is not defined
at onLocalTracks (voicechat.js:193:20)
(anonymous) @ voicechat.js:153
Promise.catch (async)
startVoiceChat @ voicechat.js:152
performAudio @ voicechat.js:106
handleEvent @ converse.min.js:2
that's in chrome's developer console.
This is my config in /etc/prosody/prosody.cfg.lua
:
voicechat = {
["serviceUrl"] = "wss://beta.meet.jit.si/xmpp-websocket",
["prefix"] = "voicechat-",
["transcribe"] = false,
["transcribeLanguage"] = "en-GB",
["start"] = "Start Voice Chat",
["stop"] = "Stop Voice Chat",
["started"] = "has started speaking",
["stopped"] = "has stopped speaking",
["hosts"] =
{
["domain"] = "beta.meet.jit.si",
["muc"] = "conference.beta.meet.jit.si"
}
};
following Zash's advice on the prosody's MUC, I changed the start/stop/started/stopped lines like that.
Right now, users must press two buttons to send a recorded voice message: The green tick and then the Upload
button. I think it would improve the UX if the latter could be avoided, for two reasons:
Upload
button might be confusing (or even worrying) to end users, who may not be aware that uploading the message is our way of sharing it with (only) the contact/group.As I understand it, the reason for the separate Upload
button is allowing for playing back the recorded message after pressing the green tick. I think it would be nice if this was possible before pressing the green tick, so the green tick would upload the message directly. If this is not an option (or too much work), I'd suggest at least renaming the Upload
knob to Send
in order to address the second issue I mentioned.
...but available in MUCs
Using Converse & plugins HEAD
Hello,
I'm trying to install the "actions" plugin in conversejs under prosody.
I've added the following option to /etc/prosody/prosody.cfg.lua
conversejs_options = {
....
whitelisted_plugins = {"actions"};
}
but I don't know where to place the plugins files..
Thanks
There is actually an experimental XEP for collaborative XML editing through a MUC, see:
https://xmpp.org/extensions/xep-0284.html
Quite a bit ago there was an implementation for it, but it never really took off:
https://hg.linkmauve.fr/eldonilo/barbecue
Alternatively this framework seems to be a bit more extensive and also has an XMPP Muc connector:
https://github.com/y-js/yjs
Might be a cool plugin for ConverseJS.
For community plugins, it is possible to have a XEPs and RFCs support page with version (XEP-XXXX v1.2)?
Examples:
About main project, it is here:
As seen in #21 after using type chat
the plugin works in 1:1, but as the description implies, only for messages from MAM.
MAM messages means unencrypted messages, while I have on screen plenty of text, the plugin doesn't find anything....that was OMEMO encrypted.
Hope the plugin can be extended to include local DB saved messages too.
Converse HEAD (conversejs/converse.js@4aa6b72), Search plugin HEAD (37aa611), ejabberd HEAD
Search in MUCs, everything ok (eg. ejabberd MUC)
Search in a 1:1, log says WARNING: Did not fetch MAM archive for [email protected] because it doesn't support urn:xmpp:mam:2
What am I missing?
..as Backbone has been removed on HEAD
log error says:
TypeError: converse.env.Backbone is undefinedsearch.js:23:55
initialize https://convorb.im/plugins/search/search.js:23
This makes _converse a global, which you don't want. It contains the inner, private API only available to plugins.
community-plugins/directory/directory.js
Line 18 in 96430ab
Might be a nice combination with the audioconf plugin to get a pretty cool BigBlueButton alternative.
https://github.com/spacedeck/spacedeck-open
Looks cool, but there might be other simpler options.
Thoughts? Ideas?
Edit: Yeah I guess that there is also Openfire-meetings?
(function (root, factory) {
if (typeof define === 'function' && define.amd) {
// AMD. Register as a module called "myplugin"
define(["converse"], factory);
} else {
// Browser globals. If you're not using a module loader such as require.js,
// then this line below executes. Make sure that your plugin's <script> tag
// appears after the one from converse.js.
factory(converse);
}
}(this, function (converse) {
// Commonly used utilities and variables can be found under the "env"
// namespace of the "converse" global.
var Strophe = converse.env.Strophe,
$iq = converse.env.$iq,
$msg = converse.env.$msg,
$pres = converse.env.$pres,
$build = converse.env.$build,
b64_sha1 = converse.env.b64_sha1,
_ = converse.env._,
moment = converse.env.moment;
var _converse = null, baseUrl = null, default_domain = null, bosh = null, muc = null;
var conferenceView;
var options ;
var confOptions = {
openBridgeChannel: true,
p2p: {
// Enables peer to peer mode. When enabled the system will try to
// establish a direct connection when there are exactly 2 participants
// in the room. If that succeeds the conference will stop sending data
// through the JVB and use the peer to peer connection instead. When a
// 3rd participant joins the conference will be moved back to the JVB
// connection.
enabled: false,
// Use XEP-0215 to fetch STUN and TURN servers.
// useStunTurn: true,
// The STUN servers that will be used in the peer to peer connections
stunServers: [
{ urls: 'stun:stun.l.google.com:19302' },
{ urls: 'stun:stun1.l.google.com:19302' },
{ urls: 'stun:stun2.l.google.com:19302' }
],
// If set to true, it will prefer to use H.264 for P2P calls (if H.264
// is supported).
preferH264: true
// If set to true, disable H.264 video codec by stripping it out of the
// SDP.
// disableH264: false,
// How long we're going to wait, before going back to P2P after the 3rd
// participant has left the conference (to filter out page reload).
// backToP2PDelay: 5
}
};
var connection = null;
var isJoined = false;
var room = null;
var localTracks = [];
var remoteTracks = {};
var initOptions = {
disableAudioLevels: true,
// The ID of the jidesha extension for Chrome.
desktopSharingChromeExtId: 'mbocklcggfhnbahlnepmldehdhpjfcjp',
// Whether desktop sharing should be disabled on Chrome.
desktopSharingChromeDisabled: false,
// The media sources to use when using screen sharing with the Chrome
// extension.
desktopSharingChromeSources: [ 'screen', 'window' ],
// Required version of Chrome extension
desktopSharingChromeMinExtVersion: '0.1',
// Whether desktop sharing should be disabled on Firefox.
desktopSharingFirefoxDisabled: true
};
var jid;
var passeord;
// The following line registers your plugin.
converse.plugins.add("jitsimeet", {
/* Dependencies are other plugins which might be
* overridden or relied upon, and therefore need to be loaded before
* this plugin. They are "optional" because they might not be
* available, in which case any overrides applicable to them will be
* ignored.
*
* NB: These plugins need to have already been loaded via require.js.
*
* It's possible to make these dependencies "non-optional".
* If the setting "strict_plugin_dependencies" is set to true,
* an error will be raised if the plugin is not found.
*/
'dependencies': ['converse-roomslist','converse-muc-views'],
/* Converse.js's plugin mechanism will call the initialize
* method on any plugin (if it exists) as soon as the plugin has
* been loaded.
*/
'initialize': function () {
/* Inside this method, you have access to the private
* `_converse` object.
*/
_converse = this._converse;
// _converse.log("The \"myplugin\" plugin is being initialized");
/* From the `_converse` object you can get any configuration
* options that the user might have passed in via
* `converse.initialize`.
*
* You can also specify new configuration settings for this
* plugin, or override the default values of existing
* configuration settings. This is done like so:
*/
_converse.api.settings.update({
'initialize_message': 'Initializing myplugin!'
});
/* The user can then pass in values for the configuration
* settings when `converse.initialize` gets called.
* For example:
*
* converse.initialize({
* "initialize_message": "My plugin has been initialized"
* });
*/
// alert(this._converse.initialize_message);
/* Besides `_converse.api.settings.update`, there is also a
* `_converse.api.promises.add` method, which allows you to
* add new promises that your plugin is obligated to fulfill.
*
* This method takes a string or a list of strings which
* represent the promise names:
*
* _converse.api.promises.add('myPromise');
*
* Your plugin should then, when appropriate, resolve the
* promise by calling `_converse.api.emit`, which will also
* emit an event with the same name as the promise.
* For example:
*
* _converse.api.emit('operationCompleted');
*
* Other plugins can then either listen for the event
* `operationCompleted` like so:
*
* _converse.api.listen.on('operationCompleted', function { ... });
*
* or they can wait for the promise to be fulfilled like so:
*
* _converse.api.waitUntil('operationCompleted', function { ... });
*/
baseUrl = location.protocol+"//" + _converse.api.settings.get("bosh_service_url").split("/")[2];
bosh = '//'+_converse.api.settings.get("bosh_service_url").split("/")[2]+"/http-bind";
default_domain = _converse.api.settings.get("default_domain");
muc = _converse.api.settings.get("muc_domain")
console.log('connection',_converse.connection);
options = {
hosts: {
domain: default_domain,
muc: muc // FIXME: use XEP-0030
},
bosh: bosh, // FIXME: use xep-0156 for that
// The name of client node advertised in XEP-0115 'c' stanza
clientNode: 'http://jitsi.org/jitsimeet'
};
videoDialog = _converse.BootstrapModal.extend({
initialize() {
_converse.BootstrapModal.prototype.initialize.apply(this, arguments);
// this.model.on('change', this.render, this);
JitsiMeetJS.init(initOptions);
JitsiMeetJS.setLogLevel(JitsiMeetJS.logLevels.DEBUG);
jitisInitalize()
},
toHTML() {
return '<div class="modal" id="myModal"> <div class="modal-dialog modal-lg"> <div class="modal-content">' +
'<div class="modal-body">' +
'</div>' +
'</div> </div>' +
'<div style="width:100px;height:100px" id="localVideo"> </div>' +
'</div>';
},
});
function jitisInitalize(){
connection = new JitsiMeetJS.JitsiConnection(null, null, options);
console.log('connecttion jitsi meet',connection)
connection.addEventListener(
JitsiMeetJS.events.connection.CONNECTION_ESTABLISHED,
onConnectionSuccess);
connection.addEventListener(
JitsiMeetJS.events.connection.CONNECTION_FAILED,
onConnectionFailed);
connection.addEventListener(
JitsiMeetJS.events.connection.CONNECTION_DISCONNECTED,
disconnect);
JitsiMeetJS.mediaDevices.addEventListener(
JitsiMeetJS.events.mediaDevices.DEVICE_LIST_CHANGED,
onDeviceListChanged);
console.log('passwrd',password)
//>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
connection.connect({id:_converse.connection.jid.split('/')[0],password:_converse.connection.pass});
JitsiMeetJS.createLocalTracks({ devices: [ 'audio', 'video' ] })
.then(onLocalTracks)
.catch(error => {
throw error;
});
if (JitsiMeetJS.mediaDevices.isDeviceChangeAvailable('output')) {
JitsiMeetJS.mediaDevices.enumerateDevices(devices => {
const audioOutputDevices
= devices.filter(d => d.kind === 'audiooutput');
if (audioOutputDevices.length > 1) {
$('#audioOutputSelect').html(
audioOutputDevices
.map(
d =>
`<option value="${d.deviceId}">${d.label}</option>`)
.join('\n'));
$('#audioOutputSelectWrapper').show();
}
});
}
}
function onConnectionSuccess() {
room = connection.initJitsiConference('test', confOptions);
console.log('room',room)
room.on(JitsiMeetJS.events.conference.TRACK_ADDED, onRemoteTrack);
room.on(JitsiMeetJS.events.conference.TRACK_REMOVED, track => {
console.log(`track removed!!!${track}`);
});
room.on(
JitsiMeetJS.events.conference.CONFERENCE_JOINED,
onConferenceJoined);
room.on(JitsiMeetJS.events.conference.USER_JOINED, id => {
console.log('user join');
remoteTracks[id] = [];
});
room.on(JitsiMeetJS.events.conference.USER_LEFT, onUserLeft);
room.on(JitsiMeetJS.events.conference.TRACK_MUTE_CHANGED, track => {
console.log(`${track.getType()} - ${track.isMuted()}`);
});
room.on(
JitsiMeetJS.events.conference.DISPLAY_NAME_CHANGED,
(userID, displayName) => console.log(`${userID} - ${displayName}`));
room.on(
JitsiMeetJS.events.conference.TRACK_AUDIO_LEVEL_CHANGED,
(userID, audioLevel) => console.log(`${userID} - ${audioLevel}`));
room.on(
JitsiMeetJS.events.conference.PHONE_NUMBER_CHANGED,
() => console.log(`${room.getPhoneNumber()} - ${room.getPhonePin()}`));
room.join();
}
function onRemoteTrack(track) {
if (track.isLocal()) {
return;
}
const participant = track.getParticipantId();
if (!remoteTracks[participant]) {
remoteTracks[participant] = [];
}
const idx = remoteTracks[participant].push(track);
track.addEventListener(
JitsiMeetJS.events.track.TRACK_AUDIO_LEVEL_CHANGED,
audioLevel => console.log(`Audio Level remote: ${audioLevel}`));
track.addEventListener(
JitsiMeetJS.events.track.TRACK_MUTE_CHANGED,
() => console.log('remote track muted'));
track.addEventListener(
JitsiMeetJS.events.track.LOCAL_TRACK_STOPPED,
() => console.log('remote track stoped'));
track.addEventListener(JitsiMeetJS.events.track.TRACK_AUDIO_OUTPUT_CHANGED,
deviceId =>
console.log(
`track audio output device was changed to ${deviceId}`));
const id = participant + track.getType() + idx;
if (track.getType() === 'video') {
$('#localVideo').append(
`<video style="width:100px;height:100px" autoplay='1' id='${participant}video${idx}' />`);
} else {
$('#localVideo').append(
`<audio autoplay='1' id='${participant}audio${idx}' />`);
}
track.attach($(`#${id}`)[0]);
}
function onLocalTracks(tracks) {
localTracks = tracks;
for (let i = 0; i < localTracks.length; i++) {
localTracks[i].addEventListener(
JitsiMeetJS.events.track.TRACK_AUDIO_LEVEL_CHANGED,
audioLevel => console.log(`Audio Level local: ${audioLevel}`));
localTracks[i].addEventListener(
JitsiMeetJS.events.track.TRACK_MUTE_CHANGED,
() => console.log('local track muted'));
localTracks[i].addEventListener(
JitsiMeetJS.events.track.LOCAL_TRACK_STOPPED,
() => console.log('local track stoped'));
localTracks[i].addEventListener(
JitsiMeetJS.events.track.TRACK_AUDIO_OUTPUT_CHANGED,
deviceId =>
console.log(
`track audio output device was changed to ${deviceId}`));
if (localTracks[i].getType() === 'video') {
$('#localVideo').append(`<video autoplay='1' style="width:100px;height:100px;margin-left:500px" id='localVideo${i}' />`);
console.log('localVideo',$('localVideo'),i);
localTracks[i].attach($(`#localVideo${i}`)[0]);
} else {
$('#localVideo').append(
`<audio autoplay='1' muted='true' id='localAudio${i}' />`);
localTracks[i].attach($(`#localAudio${i}`)[0]);
}
if (isJoined) {
room.addTrack(localTracks[i]);
}
}
}
function disconnect() {
console.log('disconnect!');
connection.removeEventListener(
JitsiMeetJS.events.connection.CONNECTION_ESTABLISHED,
onConnectionSuccess);
connection.removeEventListener(
JitsiMeetJS.events.connection.CONNECTION_FAILED,
onConnectionFailed);
connection.removeEventListener(
JitsiMeetJS.events.connection.CONNECTION_DISCONNECTED,
disconnect);
}
function onDeviceListChanged(devices) {
console.info('current devices', devices);
}
function onConnectionFailed() {
console.error('Connection Failed!');
}
function onConferenceJoined() {
console.log('conference joined!');
isJoined = true;
for (let i = 0; i < localTracks.length; i++) {
room.addTrack(localTracks[i]);
}
}
function onUserLeft(id) {
console.log('user left');
if (!remoteTracks[id]) {
return;
}
const tracks = remoteTracks[id];
for (let i = 0; i < tracks.length; i++) {
tracks[i].detach($(`#${id}${tracks[i].getType()}`));
}
}
},
/* If you want to override some function or a Backbone model or
* view defined elsewhere in converse.js, then you do that under
* the "overrides" namespace.
*/
'overrides': {
/* For example, the private *_converse* object has a
* method "onConnected". You can override that method as follows:
*/
'onConnected': function () {
// Overrides the onConnected method in converse.js
// Top-level functions in "overrides" are bound to the
// inner "_converse" object.
var _converse = this;
jid = _converse.connection.jid
password = _converse.connection.password
// Your custom code can come here ...
// You can access the original function being overridden
// via the __super__ attribute.
// Make sure to pass on the arguments supplied to this
// function and also to apply the proper "this" object.
_converse.__super__.onConnected.apply(this, arguments);
// Your custom code can come here ...
},
ChatRoomView:{
initialize(){
var _converse = this;
this.createConferenceView();
_converse.__super__.initialize.apply(this, arguments);
},
createConferenceView(){
console.log('create conference view',_converse.connection.pass);
},
videoCall(){
videoDialog = new videoDialog();
videoDialog.show();
}
},
/* Override converse.js's XMPPStatus Backbone model so that we can override the
* function that sends out the presence stanza.
*/
'XMPPStatus': {
'sendPresence': function (type, status_message, jid) {
// The "_converse" object is available via the __super__
// attribute.
var _converse = this.__super__._converse;
// Custom code can come here ...
// You can call the original overridden method, by
// accessing it via the __super__ attribute.
// When calling it, you need to apply the proper
// context as reference by the "this" variable.
this.__super__.sendPresence.apply(this, arguments);
// Custom code can come here ...
}
}
}
});
function newElement(el, id, html)
{
var ele = document.createElement(el);
if (id) ele.id = id;
if (html) ele.innerHTML = html;
document.body.appendChild(ele);
return ele;
}
}));
I am trying to get password of user in the jitisInitalize function. but its undefine. could you help me ?
Hi,
I'm trying the screencast plugin in my LAN with 2 desktop PCs. Problem is, after I start casting from one PC, nothing loads on the second and vice versa.. So it looks like it's casting from one side (I have the popup window and can select what to cast), but nothing really happens in destination.
Right now the actions plugin seems to have stopped working with ConverseJS 9.1.0. Edit: Works with 10.0.0
Also would be cool if it could support: https://xmpp.org/extensions/xep-0461.html
XEP-0461 does the message-styling as a fallback similar to what is currently implemented.
Movim & Moxxy2 already have support for XEP-0461 and I think other clients will follow.
In some browsers or platforms the PDF functionality will not work, thus the button is a bit confusing. Would be nice to optionally disable/hide it.
An alternative might be a Print button as that is more widely supported and can also be used to make pdfs.
Moving issue conversejs/converse.js#3016 here
Cool idea for the muc-directory plugin.
Could you optionally add the function to query https://search.jabber.network instead?
Clients like Conversations or Movim are doing that.
Just needs a warning that a 3rd party source is used.
A declarative, efficient, and flexible JavaScript library for building user interfaces.
๐ Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. ๐๐๐
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google โค๏ธ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.